mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add external json playout * basic local playback works * fallback to streaming from plex * update external json file * update changelogpull/1540/head
38 changed files with 5420 additions and 124 deletions
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Channel = ErsatzTV.Core.Domain.Channel; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public class CreateExternalJsonPlayoutHandler |
||||
: IRequestHandler<CreateExternalJsonPlayout, Either<BaseError, CreatePlayoutResponse>> |
||||
{ |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public CreateExternalJsonPlayoutHandler( |
||||
ILocalFileSystem localFileSystem, |
||||
ChannelWriter<IBackgroundServiceRequest> channel, |
||||
IDbContextFactory<TvContext> dbContextFactory) |
||||
{ |
||||
_localFileSystem = localFileSystem; |
||||
_channel = channel; |
||||
_dbContextFactory = dbContextFactory; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle( |
||||
CreateExternalJsonPlayout request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(playout => PersistPlayout(dbContext, playout)); |
||||
} |
||||
|
||||
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout) |
||||
{ |
||||
await dbContext.Playouts.AddAsync(playout); |
||||
await dbContext.SaveChangesAsync(); |
||||
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); |
||||
await _channel.WriteAsync(new RefreshChannelList()); |
||||
return new CreatePlayoutResponse(playout.Id); |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate( |
||||
TvContext dbContext, |
||||
CreateExternalJsonPlayout request) => |
||||
(await ValidateChannel(dbContext, request), ValidateExternalJsonFile(request), ValidatePlayoutType(request)) |
||||
.Apply( |
||||
(channel, externalJsonFile, playoutType) => new Playout |
||||
{ |
||||
ChannelId = channel.Id, |
||||
ExternalJsonFile = externalJsonFile, |
||||
ProgramSchedulePlayoutType = playoutType |
||||
}); |
||||
|
||||
private static Task<Validation<BaseError, Channel>> ValidateChannel( |
||||
TvContext dbContext, |
||||
CreateExternalJsonPlayout createExternalJsonPlayout) => |
||||
dbContext.Channels |
||||
.Include(c => c.Playouts) |
||||
.SelectOneAsync(c => c.Id, c => c.Id == createExternalJsonPlayout.ChannelId) |
||||
.Map(o => o.ToValidation<BaseError>("Channel does not exist")) |
||||
.BindT(ChannelMustNotHavePlayouts); |
||||
|
||||
private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) => |
||||
Optional(channel.Playouts.Count) |
||||
.Filter(count => count == 0) |
||||
.Map(_ => channel) |
||||
.ToValidation<BaseError>("Channel already has one playout"); |
||||
|
||||
private Validation<BaseError, string> ValidateExternalJsonFile(CreateExternalJsonPlayout request) |
||||
{ |
||||
if (!_localFileSystem.FileExists(request.ExternalJsonFile)) |
||||
{ |
||||
return BaseError.New("External Json File does not exist!"); |
||||
} |
||||
|
||||
return request.ExternalJsonFile; |
||||
} |
||||
|
||||
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( |
||||
CreateExternalJsonPlayout createExternalJsonPlayout) => |
||||
Optional(createExternalJsonPlayout.ProgramSchedulePlayoutType) |
||||
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.ExternalJson) |
||||
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be ExternalJson"); |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public record UpdateExternalJsonPlayout(int PlayoutId, string ExternalJsonFile) |
||||
: IRequest<Either<BaseError, PlayoutNameViewModel>>; |
||||
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public class UpdateExternalJsonPlayoutHandler : IRequestHandler<UpdateExternalJsonPlayout, Either<BaseError, PlayoutNameViewModel>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public UpdateExternalJsonPlayoutHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<Either<BaseError, PlayoutNameViewModel>> Handle( |
||||
UpdateExternalJsonPlayout request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout)); |
||||
} |
||||
|
||||
private static async Task<PlayoutNameViewModel> ApplyUpdateRequest( |
||||
TvContext dbContext, |
||||
UpdateExternalJsonPlayout request, |
||||
Playout playout) |
||||
{ |
||||
playout.ExternalJsonFile = request.ExternalJsonFile; |
||||
|
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
return new PlayoutNameViewModel( |
||||
playout.Id, |
||||
playout.ProgramSchedulePlayoutType, |
||||
playout.Channel.Name, |
||||
playout.Channel.Number, |
||||
playout.ProgramSchedule?.Name ?? string.Empty, |
||||
playout.ExternalJsonFile, |
||||
Optional(playout.DailyRebuildTime)); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, UpdateExternalJsonPlayout request) => |
||||
PlayoutMustExist(dbContext, request); |
||||
|
||||
private static Task<Validation<BaseError, Playout>> PlayoutMustExist( |
||||
TvContext dbContext, |
||||
UpdateExternalJsonPlayout updatePlayout) => |
||||
dbContext.Playouts |
||||
.Include(p => p.Channel) |
||||
.SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId) |
||||
.Map(o => o.ToValidation<BaseError>("Playout does not exist.")); |
||||
} |
||||
@ -1,8 +1,12 @@
@@ -1,8 +1,12 @@
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public record PlayoutNameViewModel( |
||||
int PlayoutId, |
||||
ProgramSchedulePlayoutType PlayoutType, |
||||
string ChannelName, |
||||
string ChannelNumber, |
||||
string ScheduleName, |
||||
string ExternalJsonFile, |
||||
Option<TimeSpan> DailyRebuildTime); |
||||
|
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
public sealed record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path); |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Streaming; |
||||
|
||||
public interface IExternalJsonPlayoutItemProvider |
||||
{ |
||||
Task<Either<BaseError, PlayoutItemWithPath>> CheckForExternalJson( |
||||
Channel channel, |
||||
DateTimeOffset now, |
||||
string ffmpegPath, |
||||
string ffprobePath); |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace ErsatzTV.Core.Streaming; |
||||
|
||||
public class ExternalJsonChannel |
||||
{ |
||||
[JsonProperty("startTime")] |
||||
public string StartTime { get; set; } |
||||
|
||||
public ExternalJsonProgram[] Programs { get; set; } |
||||
} |
||||
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace ErsatzTV.Core.Streaming; |
||||
|
||||
public class ExternalJsonProgram |
||||
{ |
||||
[JsonProperty("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[JsonProperty("showTitle")] |
||||
public string ShowTitle { get; set; } |
||||
|
||||
[JsonProperty("season")] |
||||
public int Season { get; set; } |
||||
|
||||
[JsonProperty("episode")] |
||||
public int Episode { get; set; } |
||||
|
||||
[JsonProperty("key")] |
||||
public string Key { get; set; } |
||||
|
||||
[JsonProperty("ratingKey")] |
||||
public string RatingKey { get; set; } |
||||
|
||||
[JsonProperty("type")] |
||||
public string Type { get; set; } |
||||
|
||||
[JsonProperty("duration")] |
||||
public int Duration { get; set; } |
||||
|
||||
[JsonProperty("plexFile")] |
||||
public string PlexFile { get; set; } |
||||
|
||||
[JsonProperty("file")] |
||||
public string File { get; set; } |
||||
|
||||
[JsonProperty("serverKey")] |
||||
public string ServerKey { get; set; } |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class AddExternalJsonPlayout : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AlterColumn<int>( |
||||
name: "ProgramScheduleId", |
||||
table: "Playout", |
||||
type: "INTEGER", |
||||
nullable: true, |
||||
oldClrType: typeof(int), |
||||
oldType: "INTEGER"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "ExternalJsonFile", |
||||
table: "Playout", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "ExternalJsonFile", |
||||
table: "Playout"); |
||||
|
||||
migrationBuilder.AlterColumn<int>( |
||||
name: "ProgramScheduleId", |
||||
table: "Playout", |
||||
type: "INTEGER", |
||||
nullable: false, |
||||
defaultValue: 0, |
||||
oldClrType: typeof(int), |
||||
oldType: "INTEGER", |
||||
oldNullable: true); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,302 @@
@@ -0,0 +1,302 @@
|
||||
using System.Globalization; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Errors; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Plex; |
||||
using ErsatzTV.Core.Interfaces.Streaming; |
||||
using ErsatzTV.Core.Plex; |
||||
using ErsatzTV.Core.Streaming; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.Extensions.Logging; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace ErsatzTV.Scanner.Application.Streaming; |
||||
|
||||
public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
private readonly IPlexPathReplacementService _plexPathReplacementService; |
||||
private readonly IPlexServerApiClient _plexServerApiClient; |
||||
private readonly IPlexSecretStore _plexSecretStore; |
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider; |
||||
private readonly ILogger<ExternalJsonPlayoutItemProvider> _logger; |
||||
|
||||
public ExternalJsonPlayoutItemProvider( |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
ILocalFileSystem localFileSystem, |
||||
IPlexPathReplacementService plexPathReplacementService, |
||||
IPlexServerApiClient plexServerApiClient, |
||||
IPlexSecretStore plexSecretStore, |
||||
ILocalStatisticsProvider localStatisticsProvider, |
||||
ILogger<ExternalJsonPlayoutItemProvider> logger) |
||||
{ |
||||
_dbContextFactory = dbContextFactory; |
||||
_localFileSystem = localFileSystem; |
||||
_plexPathReplacementService = plexPathReplacementService; |
||||
_plexServerApiClient = plexServerApiClient; |
||||
_plexSecretStore = plexSecretStore; |
||||
_localStatisticsProvider = localStatisticsProvider; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, PlayoutItemWithPath>> CheckForExternalJson( |
||||
Channel channel, |
||||
DateTimeOffset now, |
||||
string ffmpegPath, |
||||
string ffprobePath) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts |
||||
.AsNoTracking() |
||||
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == channel.Id); |
||||
|
||||
foreach (Playout playout in maybePlayout) |
||||
{ |
||||
// playout must be external json
|
||||
if (playout.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.ExternalJson) |
||||
{ |
||||
// json file must exist
|
||||
if (_localFileSystem.FileExists(playout.ExternalJsonFile)) |
||||
{ |
||||
return await GetExternalJsonPlayoutItem(dbContext, playout, now, ffmpegPath, ffprobePath); |
||||
} |
||||
|
||||
_logger.LogWarning( |
||||
"Unable to locate external json file {File} for channel {Number} - {Name}", |
||||
playout.ExternalJsonFile, |
||||
channel.Number, |
||||
channel.Name); |
||||
} |
||||
} |
||||
|
||||
return new UnableToLocatePlayoutItem(); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> GetExternalJsonPlayoutItem( |
||||
TvContext dbContext, |
||||
Playout playout, |
||||
DateTimeOffset now, |
||||
string ffmpegPath, |
||||
string ffprobePath) |
||||
{ |
||||
Option<ExternalJsonChannel> maybeChannel = JsonConvert.DeserializeObject<ExternalJsonChannel>( |
||||
await File.ReadAllTextAsync(playout.ExternalJsonFile)); |
||||
|
||||
// must deserialize channel from json
|
||||
foreach (ExternalJsonChannel channel in maybeChannel) |
||||
{ |
||||
// TODO: null start time should log and throw
|
||||
|
||||
DateTimeOffset startTime = DateTimeOffset.Parse( |
||||
channel.StartTime ?? string.Empty, |
||||
CultureInfo.InvariantCulture, |
||||
DateTimeStyles.AssumeUniversal).ToLocalTime(); |
||||
|
||||
//_logger.LogDebug("external json start time: {StartTime}", startTime);
|
||||
|
||||
foreach (ExternalJsonProgram program in channel.Programs) |
||||
{ |
||||
int milliseconds = program.Duration; |
||||
DateTimeOffset nextStart = startTime + TimeSpan.FromMilliseconds(milliseconds); |
||||
if (nextStart > now) |
||||
{ |
||||
//_logger.LogDebug("should play program {@Program}", program);
|
||||
return await BuildPlayoutItem(dbContext, startTime, program, ffmpegPath, ffprobePath); |
||||
} |
||||
|
||||
startTime = nextStart; |
||||
} |
||||
} |
||||
|
||||
return new UnableToLocatePlayoutItem(); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> BuildPlayoutItem( |
||||
TvContext dbContext, |
||||
DateTimeOffset startTime, |
||||
ExternalJsonProgram program, |
||||
string ffmpegPath, |
||||
string ffprobePath) |
||||
{ |
||||
// find any library path from the appropriate plex server
|
||||
List<LibraryPath> maybeLibraryPath = await dbContext.LibraryPaths |
||||
.Filter(lp => ((PlexMediaSource)((PlexLibrary)lp.Library).MediaSource).ServerName == program.ServerKey) |
||||
.OrderBy(lp => lp.Id) |
||||
.Take(1) |
||||
.ToListAsync(); |
||||
|
||||
foreach (LibraryPath libraryPath in maybeLibraryPath.HeadOrNone()) |
||||
{ |
||||
string localPath = await _plexPathReplacementService.GetReplacementPlexPath( |
||||
libraryPath.Id, |
||||
program.File); |
||||
|
||||
if (_localFileSystem.FileExists(localPath)) |
||||
{ |
||||
return await StreamLocally(startTime, program, ffmpegPath, ffprobePath, localPath); |
||||
} |
||||
|
||||
return await StreamRemotely(dbContext, startTime, program); |
||||
} |
||||
|
||||
return new UnableToLocatePlayoutItem(); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> StreamLocally( |
||||
DateTimeOffset startTime, |
||||
ExternalJsonProgram program, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
string localPath) |
||||
{ |
||||
// ffprobe on demand
|
||||
Either<BaseError, MediaVersion> maybeMediaVersion = |
||||
await _localStatisticsProvider.GetStatistics(ffmpegPath, ffprobePath, localPath); |
||||
|
||||
foreach (MediaVersion mediaVersion in maybeMediaVersion.RightToSeq()) |
||||
{ |
||||
// build playout item
|
||||
var episode = new Episode |
||||
{ |
||||
MediaVersions = [mediaVersion], |
||||
EpisodeMetadata = |
||||
[ |
||||
new EpisodeMetadata |
||||
{ |
||||
EpisodeNumber = program.Episode, |
||||
Title = program.Title, |
||||
}, |
||||
], |
||||
Season = new Season |
||||
{ |
||||
SeasonNumber = program.Season, |
||||
Show = new Show |
||||
{ |
||||
ShowMetadata = |
||||
[ |
||||
new ShowMetadata |
||||
{ |
||||
Title = program.ShowTitle |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}; |
||||
|
||||
return new PlayoutItemWithPath(GetPlayoutItem(startTime, episode, program), localPath); |
||||
} |
||||
|
||||
return new UnableToLocatePlayoutItem(); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> StreamRemotely( |
||||
TvContext dbContext, |
||||
DateTimeOffset startTime, |
||||
ExternalJsonProgram program) |
||||
{ |
||||
Option<PlexMediaSource> maybeServer = await dbContext.PlexMediaSources |
||||
.Include(pms => pms.Connections) |
||||
.SelectOneAsync(pms => pms.ServerName, pms => pms.ServerName == program.ServerKey); |
||||
|
||||
foreach (PlexMediaSource server in maybeServer) |
||||
{ |
||||
Option<PlexConnection> maybeConnection = server.Connections.SingleOrDefault(c => c.IsActive); |
||||
foreach (PlexConnection connection in maybeConnection) |
||||
{ |
||||
Option<PlexServerAuthToken> maybeToken = |
||||
await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier); |
||||
|
||||
foreach (PlexServerAuthToken token in maybeToken) |
||||
{ |
||||
MediaItem mediaItem = program.Type switch |
||||
{ |
||||
"episode" => await GetPlexEpisode(server, connection, token, program), |
||||
_ => await GetPlexMovie(server, connection, token, program) |
||||
}; |
||||
|
||||
return new PlayoutItemWithPath( |
||||
GetPlayoutItem(startTime, mediaItem, program), |
||||
$"http://localhost:{Settings.ListenPort}/media/plex/{server.Id}/{program.PlexFile}"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// TODO: log errors?
|
||||
return new UnableToLocatePlayoutItem(); |
||||
} |
||||
|
||||
private async Task<MediaItem> GetPlexEpisode( |
||||
PlexMediaSource plexMediaSource, |
||||
PlexConnection connection, |
||||
PlexServerAuthToken token, |
||||
ExternalJsonProgram program) |
||||
{ |
||||
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics = |
||||
await _plexServerApiClient.GetEpisodeMetadataAndStatistics( |
||||
plexMediaSource.Id, |
||||
program.RatingKey, |
||||
connection, |
||||
token); |
||||
|
||||
foreach (Tuple<EpisodeMetadata, MediaVersion> result in maybeStatistics.RightToSeq()) |
||||
{ |
||||
return new PlexEpisode |
||||
{ |
||||
EpisodeMetadata = [result.Item1], |
||||
MediaVersions = [result.Item2] |
||||
}; |
||||
} |
||||
|
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
private async Task<MediaItem> GetPlexMovie( |
||||
PlexMediaSource plexMediaSource, |
||||
PlexConnection connection, |
||||
PlexServerAuthToken token, |
||||
ExternalJsonProgram program) |
||||
{ |
||||
Either<BaseError, Tuple<MovieMetadata, MediaVersion>> maybeStatistics = |
||||
await _plexServerApiClient.GetMovieMetadataAndStatistics( |
||||
plexMediaSource.Id, |
||||
program.RatingKey, |
||||
connection, |
||||
token); |
||||
|
||||
foreach (Tuple<MovieMetadata, MediaVersion> result in maybeStatistics.RightToSeq()) |
||||
{ |
||||
return new PlexMovie |
||||
{ |
||||
MovieMetadata = [result.Item1], |
||||
MediaVersions = [result.Item2] |
||||
}; |
||||
} |
||||
|
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
private static PlayoutItem GetPlayoutItem( |
||||
DateTimeOffset startTime, |
||||
MediaItem mediaItem, |
||||
ExternalJsonProgram program) => |
||||
new() |
||||
{ |
||||
Start = startTime.UtcDateTime, |
||||
Finish = startTime.AddMilliseconds(program.Duration).UtcDateTime, |
||||
FillerKind = FillerKind.None, |
||||
ChapterTitle = null, |
||||
GuideFinish = null, |
||||
GuideGroup = 0, |
||||
CustomTitle = null, |
||||
InPoint = TimeSpan.Zero, |
||||
OutPoint = TimeSpan.FromMilliseconds(program.Duration), |
||||
MediaItem = mediaItem |
||||
}; |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
@implements IDisposable |
||||
|
||||
<MudDialog> |
||||
<DialogContent> |
||||
<MudContainer Class="mb-6"> |
||||
<MudText> |
||||
Edit the playout's external json file |
||||
</MudText> |
||||
</MudContainer> |
||||
<MudTextField Label="External Json File" @bind-Value="_externalJsonFile" /> |
||||
</DialogContent> |
||||
<DialogActions> |
||||
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton> |
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(string.IsNullOrWhiteSpace(_externalJsonFile))" OnClick="Submit"> |
||||
Save Changes |
||||
</MudButton> |
||||
</DialogActions> |
||||
</MudDialog> |
||||
|
||||
@code { |
||||
private readonly CancellationTokenSource _cts = new(); |
||||
|
||||
[Parameter] |
||||
public string ExternalJsonFile { get; set; } |
||||
|
||||
[CascadingParameter] |
||||
MudDialogInstance MudDialog { get; set; } |
||||
|
||||
private string _externalJsonFile; |
||||
|
||||
public void Dispose() |
||||
{ |
||||
_cts.Cancel(); |
||||
_cts.Dispose(); |
||||
} |
||||
|
||||
protected override void OnParametersSet() |
||||
{ |
||||
_externalJsonFile = ExternalJsonFile; |
||||
} |
||||
|
||||
private void Submit() => MudDialog.Close(DialogResult.Ok(_externalJsonFile)); |
||||
|
||||
private void Cancel() => MudDialog.Cancel(); |
||||
} |
||||
@ -1,15 +1,17 @@
@@ -1,15 +1,17 @@
|
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Application.Playouts; |
||||
using ErsatzTV.Application.ProgramSchedules; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.ViewModels; |
||||
|
||||
public class PlayoutEditViewModel |
||||
{ |
||||
public string Kind { get; set; } |
||||
public ChannelViewModel Channel { get; set; } |
||||
public ProgramScheduleViewModel ProgramSchedule { get; set; } |
||||
public string ExternalJsonFile { get; set; } |
||||
|
||||
public CreatePlayout ToCreate() => |
||||
new(Channel.Id, ProgramSchedule.Id, ProgramSchedulePlayoutType.Flood); |
||||
public CreatePlayout ToCreate() => Kind == "externaljson" |
||||
? new CreateExternalJsonPlayout(Channel.Id, ExternalJsonFile) |
||||
: new CreateFloodPlayout(Channel.Id, ProgramSchedule.Id); |
||||
} |
||||
|
||||
Loading…
Reference in new issue