using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Runtime; using LanguageExt; using static LanguageExt.Prelude; namespace ErsatzTV.Application.Streaming.Queries { public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler { private readonly IConfigElementRepository _configElementRepository; private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly FFmpegProcessService _ffmpegProcessService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly ILocalFileSystem _localFileSystem; private readonly IPlayoutRepository _playoutRepository; private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IRuntimeInfo _runtimeInfo; public GetPlayoutItemProcessByChannelNumberHandler( IChannelRepository channelRepository, IConfigElementRepository configElementRepository, IPlayoutRepository playoutRepository, FFmpegProcessService ffmpegProcessService, ILocalFileSystem localFileSystem, IPlexPathReplacementService plexPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService, IEmbyPathReplacementService embyPathReplacementService, IRuntimeInfo runtimeInfo) : base(channelRepository, configElementRepository) { _configElementRepository = configElementRepository; _playoutRepository = playoutRepository; _ffmpegProcessService = ffmpegProcessService; _localFileSystem = localFileSystem; _plexPathReplacementService = plexPathReplacementService; _jellyfinPathReplacementService = jellyfinPathReplacementService; _embyPathReplacementService = embyPathReplacementService; _runtimeInfo = runtimeInfo; } protected override async Task> GetProcess( GetPlayoutItemProcessByChannelNumber _, Channel channel, string ffmpegPath) { DateTimeOffset now = DateTimeOffset.Now; Either maybePlayoutItem = await _playoutRepository .GetPlayoutItem(channel.Id, now) .Map(o => o.ToEither(new UnableToLocatePlayoutItem())) .BindT(ValidatePlayoutItemPath); return await maybePlayoutItem.Match( async playoutItemWithPath => { MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) }; bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await _configElementRepository .GetValue(ConfigElementKey.FFmpegSaveReports) .Map(result => result.IfNone(false)); return Right( await _ffmpegProcessService.ForPlayoutItem( ffmpegPath, saveReports, channel, version, playoutItemWithPath.Path, playoutItemWithPath.PlayoutItem.StartOffset, now)); }, async error => { 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) { 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 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}"; 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); } } }); } private async Task> ValidatePlayoutItemPath(PlayoutItem playoutItem) { string path = await GetPlayoutItemPath(playoutItem); 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(), MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(playoutItem)) }; MediaFile file = version.MediaFiles.Head(); string path = file.Path; return playoutItem.MediaItem switch { PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath( plexMovie.LibraryPathId, path), PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath( plexEpisode.LibraryPathId, path), JellyfinMovie jellyfinMovie => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( jellyfinMovie.LibraryPathId, path), JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( jellyfinEpisode.LibraryPathId, path), EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath( embyMovie.LibraryPathId, path), EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath( embyEpisode.LibraryPathId, path), _ => path }; } private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path); } }