diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 5145494d6..d176382aa 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -6,10 +6,13 @@ using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Errors; using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; using Microsoft.Extensions.Logging; +using static LanguageExt.Prelude; namespace ErsatzTV.Application.Streaming.Queries { @@ -17,6 +20,7 @@ namespace ErsatzTV.Application.Streaming.Queries GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler { private readonly FFmpegProcessService _ffmpegProcessService; + private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IPlayoutRepository _playoutRepository; @@ -27,12 +31,14 @@ namespace ErsatzTV.Application.Streaming.Queries IPlayoutRepository playoutRepository, IMediaSourceRepository mediaSourceRepository, FFmpegProcessService ffmpegProcessService, + ILocalFileSystem localFileSystem, ILogger logger) : base(channelRepository, configElementRepository) { _playoutRepository = playoutRepository; _mediaSourceRepository = mediaSourceRepository; _ffmpegProcessService = ffmpegProcessService; + _localFileSystem = localFileSystem; _logger = logger; } @@ -42,49 +48,124 @@ namespace ErsatzTV.Application.Streaming.Queries string ffmpegPath) { DateTimeOffset now = DateTimeOffset.Now; - Option maybePlayoutItem = await _playoutRepository.GetPlayoutItem(channel.Id, now); - return await maybePlayoutItem.Match>>( - async playoutItem => + Either maybePlayoutItem = await _playoutRepository + .GetPlayoutItem(channel.Id, now) + .Map(o => o.ToEither(new UnableToLocatePlayoutItem())) + .BindT(ValidatePlayoutItemPath); + + return await maybePlayoutItem.Match( + playoutItemWithPath => { - MediaVersion version = playoutItem.MediaItem switch + MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), - _ => throw new ArgumentOutOfRangeException(nameof(playoutItem)) + _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) }; - MediaFile file = version.MediaFiles.Head(); - string path = file.Path; - if (playoutItem.MediaItem is PlexMovie plexMovie) - { - path = await GetReplacementPlexPath(plexMovie.LibraryPathId, path); - } - - return _ffmpegProcessService.ForPlayoutItem( - ffmpegPath, - channel, - version, - path, - playoutItem.StartOffset, - now); + return Right( + _ffmpegProcessService.ForPlayoutItem( + ffmpegPath, + channel, + version, + playoutItemWithPath.Path, + playoutItemWithPath.PlayoutItem.StartOffset, + now)).AsTask(); }, - async () => + async error => { - if (channel.FFmpegProfile.Transcode) + var offlineTranscodeMessage = + $"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'"; + + Option maybeDuration = await Optional(channel.FFmpegProfile.Transcode) + .Filter(transcode => transcode) + .Match( + _ => _playoutRepository.GetNextItemStart(channel.Id, now) + .MapT(nextStart => nextStart - now), + () => Option.None.AsTask()); + + switch (error) { - Option maybeDuration = await _playoutRepository.GetNextItemStart(channel.Id, now) - .MapT(nextStart => nextStart - now); + case UnableToLocatePlayoutItem: + if (channel.FFmpegProfile.Transcode) + { + return _ffmpegProcessService.ForError( + ffmpegPath, + channel, + maybeDuration, + "Channel is Offline"); + } + else + { + var message = + $"Unable to locate playout item for channel {channel.Number}; {offlineTranscodeMessage}"; - return _ffmpegProcessService.ForOfflineImage(ffmpegPath, channel, maybeDuration); - } + return BaseError.New(message); + } + case PlayoutItemDoesNotExistOnDisk: + if (channel.FFmpegProfile.Transcode) + { + return _ffmpegProcessService.ForError(ffmpegPath, channel, maybeDuration, error.Value); + } + else + { + var message = + $"Playout item does not exist on disk for channel {channel.Number}; {offlineTranscodeMessage}"; - var message = - $"Unable to locate playout item for channel {channel.Number}; offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'"; + return BaseError.New(message); + } + default: + if (channel.FFmpegProfile.Transcode) + { + return _ffmpegProcessService.ForError( + ffmpegPath, + channel, + maybeDuration, + "Channel is Offline"); + } + else + { + var message = + $"Unexpected error locating playout item for channel {channel.Number}; {offlineTranscodeMessage}"; - return BaseError.New(message); + return BaseError.New(message); + } + } }); } + private async Task> ValidatePlayoutItemPath(PlayoutItem playoutItem) + { + string path = await GetPlayoutItemPath(playoutItem); + + // TODO: this won't work with url streaming from plex + if (_localFileSystem.FileExists(path)) + { + return new PlayoutItemWithPath(playoutItem, path); + } + + return new PlayoutItemDoesNotExistOnDisk(path); + } + + private async Task GetPlayoutItemPath(PlayoutItem playoutItem) + { + MediaVersion version = playoutItem.MediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head(), + _ => throw new ArgumentOutOfRangeException(nameof(playoutItem)) + }; + + MediaFile file = version.MediaFiles.Head(); + string path = file.Path; + if (playoutItem.MediaItem is PlexMovie plexMovie) + { + path = await GetReplacementPlexPath(plexMovie.LibraryPathId, path); + } + + return path; + } + private async Task GetReplacementPlexPath(int libraryPathId, string path) { List replacements = @@ -105,5 +186,7 @@ namespace ErsatzTV.Application.Streaming.Queries }, () => path); } + + private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path); } } diff --git a/ErsatzTV.Core/Errors/PlayoutItemDoesNotExistOnDisk.cs b/ErsatzTV.Core/Errors/PlayoutItemDoesNotExistOnDisk.cs new file mode 100644 index 000000000..cea5e62a8 --- /dev/null +++ b/ErsatzTV.Core/Errors/PlayoutItemDoesNotExistOnDisk.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Errors +{ + public class PlayoutItemDoesNotExistOnDisk : BaseError + { + public PlayoutItemDoesNotExistOnDisk(string path) : base($"Playout item does not exist on disk\n{path}") + { + } + } +} diff --git a/ErsatzTV.Core/Errors/UnableToLocatePlayoutItem.cs b/ErsatzTV.Core/Errors/UnableToLocatePlayoutItem.cs new file mode 100644 index 000000000..a21035828 --- /dev/null +++ b/ErsatzTV.Core/Errors/UnableToLocatePlayoutItem.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Errors +{ + public class UnableToLocatePlayoutItem : BaseError + { + public UnableToLocatePlayoutItem() : base("Unable to locate playout item") + { + } + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index f06d4aeec..5d6aeb774 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -84,7 +84,7 @@ namespace ErsatzTV.Core.FFmpeg .Build(); } - public Process ForOfflineImage(string ffmpegPath, Channel channel, Option duration) + public Process ForError(string ffmpegPath, Channel channel, Option duration, string errorMessage) { FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); @@ -99,7 +99,7 @@ namespace ErsatzTV.Core.FFmpeg .WithLoopedImage("Resources/background.png") .WithLibavfilter() .WithInput("anullsrc") - .WithErrorText(desiredResolution, "Channel is Offline") + .WithErrorText(desiredResolution, errorMessage) .WithPixfmt("yuv420p") .WithPlaybackArgs(playbackSettings) .WithMetadata(channel) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs index b8d8500cf..e2aacefaf 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs @@ -61,8 +61,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> GetNextItemStart(int channelId, DateTimeOffset now) => _dbContext.PlayoutItems .Where(pi => pi.Playout.ChannelId == channelId) - .Where(pi => pi.Finish > now.UtcDateTime) - .OrderBy(pi => pi.Finish) + .Where(pi => pi.Start > now.UtcDateTime) + .OrderBy(pi => pi.Start) .FirstOrDefaultAsync() .Map(Optional) .MapT(pi => pi.StartOffset);