using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; 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 { public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler { private readonly IConfigElementRepository _configElementRepository; private readonly FFmpegProcessService _ffmpegProcessService; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IPlayoutRepository _playoutRepository; public GetPlayoutItemProcessByChannelNumberHandler( IChannelRepository channelRepository, IConfigElementRepository configElementRepository, IPlayoutRepository playoutRepository, IMediaSourceRepository mediaSourceRepository, FFmpegProcessService ffmpegProcessService, ILocalFileSystem localFileSystem, ILogger logger) : base(channelRepository, configElementRepository) { _configElementRepository = configElementRepository; _playoutRepository = playoutRepository; _mediaSourceRepository = mediaSourceRepository; _ffmpegProcessService = ffmpegProcessService; _localFileSystem = localFileSystem; _logger = logger; } 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(), _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) }; bool saveReports = await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports) .Map(result => result.IfNone(false)); return Right( _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); // 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; return playoutItem.MediaItem switch { PlexMovie plexMovie => await GetReplacementPlexPath(plexMovie.LibraryPathId, path), PlexEpisode plexEpisode => await GetReplacementPlexPath(plexEpisode.LibraryPathId, path), _ => path }; } private async Task GetReplacementPlexPath(int libraryPathId, string path) { List replacements = await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId); // TODO: this might barf mixing platforms (i.e. plex on linux, etv on windows) Option maybeReplacement = replacements .SingleOrDefault(r => path.StartsWith(r.PlexPath + Path.DirectorySeparatorChar)); return maybeReplacement.Match( replacement => { string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath); _logger.LogInformation( "Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}", replacement.PlexPath, replacement.LocalPath, finalPath); return finalPath; }, () => path); } private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path); } }