@ -6,10 +6,13 @@ using System.Linq;
using System.Threading.Tasks ;
using System.Threading.Tasks ;
using ErsatzTV.Core ;
using ErsatzTV.Core ;
using ErsatzTV.Core.Domain ;
using ErsatzTV.Core.Domain ;
using ErsatzTV.Core.Errors ;
using ErsatzTV.Core.FFmpeg ;
using ErsatzTV.Core.FFmpeg ;
using ErsatzTV.Core.Interfaces.Metadata ;
using ErsatzTV.Core.Interfaces.Repositories ;
using ErsatzTV.Core.Interfaces.Repositories ;
using LanguageExt ;
using LanguageExt ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Logging ;
using static LanguageExt . Prelude ;
namespace ErsatzTV.Application.Streaming.Queries
namespace ErsatzTV.Application.Streaming.Queries
{
{
@ -17,6 +20,7 @@ namespace ErsatzTV.Application.Streaming.Queries
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler < GetPlayoutItemProcessByChannelNumber >
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler < GetPlayoutItemProcessByChannelNumber >
{
{
private readonly FFmpegProcessService _f fmpegProcessService ;
private readonly FFmpegProcessService _f fmpegProcessService ;
private readonly ILocalFileSystem _l ocalFileSystem ;
private readonly ILogger < GetPlayoutItemProcessByChannelNumberHandler > _l ogger ;
private readonly ILogger < GetPlayoutItemProcessByChannelNumberHandler > _l ogger ;
private readonly IMediaSourceRepository _ mediaSourceRepository ;
private readonly IMediaSourceRepository _ mediaSourceRepository ;
private readonly IPlayoutRepository _ playoutRepository ;
private readonly IPlayoutRepository _ playoutRepository ;
@ -27,12 +31,14 @@ namespace ErsatzTV.Application.Streaming.Queries
IPlayoutRepository playoutRepository ,
IPlayoutRepository playoutRepository ,
IMediaSourceRepository mediaSourceRepository ,
IMediaSourceRepository mediaSourceRepository ,
FFmpegProcessService ffmpegProcessService ,
FFmpegProcessService ffmpegProcessService ,
ILocalFileSystem localFileSystem ,
ILogger < GetPlayoutItemProcessByChannelNumberHandler > logger )
ILogger < GetPlayoutItemProcessByChannelNumberHandler > logger )
: base ( channelRepository , configElementRepository )
: base ( channelRepository , configElementRepository )
{
{
_ playoutRepository = playoutRepository ;
_ playoutRepository = playoutRepository ;
_ mediaSourceRepository = mediaSourceRepository ;
_ mediaSourceRepository = mediaSourceRepository ;
_f fmpegProcessService = ffmpegProcessService ;
_f fmpegProcessService = ffmpegProcessService ;
_l ocalFileSystem = localFileSystem ;
_l ogger = logger ;
_l ogger = logger ;
}
}
@ -42,49 +48,124 @@ namespace ErsatzTV.Application.Streaming.Queries
string ffmpegPath )
string ffmpegPath )
{
{
DateTimeOffset now = DateTimeOffset . Now ;
DateTimeOffset now = DateTimeOffset . Now ;
Option < PlayoutItem > maybePlayoutItem = await _ playoutRepository . GetPlayoutItem ( channel . Id , now ) ;
Either < BaseError , PlayoutItemWithPath > maybePlayoutItem = await _ playoutRepository
return await maybePlayoutItem . Match < Task < Either < BaseError , Process > > > (
. GetPlayoutItem ( channel . Id , now )
async playoutItem = >
. Map ( o = > o . ToEither < BaseError > ( 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 ( ) ,
Movie m = > m . MediaVersions . Head ( ) ,
Episode e = > e . MediaVersions . Head ( ) ,
Episode e = > e . MediaVersions . Head ( ) ,
_ = > throw new ArgumentOutOfRangeException ( nameof ( playoutItem ) )
_ = > throw new ArgumentOutOfRangeException ( nameof ( playoutItemWithPath ) )
} ;
} ;
MediaFile file = version . MediaFiles . Head ( ) ;
return Right < BaseError , Process > (
string path = file . Path ;
_f fmpegProcessService . ForPlayoutItem (
if ( playoutItem . MediaItem is PlexMovie plexMovie )
ffmpegPath ,
{
channel ,
path = await GetReplacementPlexPath ( plexMovie . LibraryPathId , path ) ;
version ,
}
playoutItemWithPath . Path ,
playoutItemWithPath . PlayoutItem . StartOffset ,
return _f fmpegProcessService . ForPlayoutItem (
now ) ) . AsTask ( ) ;
ffmpegPath ,
channel ,
version ,
path ,
playoutItem . StartOffset ,
now ) ;
} ,
} ,
async ( ) = >
async error = >
{
{
if ( channel . FFmpegProfile . Transcode )
var offlineTranscodeMessage =
$"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'" ;
Option < TimeSpan > maybeDuration = await Optional ( channel . FFmpegProfile . Transcode )
. Filter ( transcode = > transcode )
. Match (
_ = > _ playoutRepository . GetNextItemStart ( channel . Id , now )
. MapT ( nextStart = > nextStart - now ) ,
( ) = > Option < TimeSpan > . None . AsTask ( ) ) ;
switch ( error )
{
{
Option < TimeSpan > maybeDuration = await _ playoutRepository . GetNextItemStart ( channel . Id , now )
case UnableToLocatePlayoutItem :
. MapT ( nextStart = > nextStart - now ) ;
if ( channel . FFmpegProfile . Transcode )
{
return _f fmpegProcessService . ForError (
ffmpegPath ,
channel ,
maybeDuration ,
"Channel is Offline" ) ;
}
else
{
var message =
$"Unable to locate playout item for channel {channel.Number}; {offlineTranscodeMessage}" ;
return _f fmpegProcessService . ForOfflineImage ( ffmpegPath , channel , maybeDuration ) ;
return BaseError . New ( message ) ;
}
}
case PlayoutItemDoesNotExistOnDisk :
if ( channel . FFmpegProfile . Transcode )
{
return _f fmpegProcessService . ForError ( ffmpegPath , channel , maybeDuration , error . Value ) ;
}
else
{
var message =
$"Playout item does not exist on disk for channel {channel.Number}; {offlineTranscodeMessage}" ;
var message =
return BaseError . New ( message ) ;
$"Unable to locate playout item for channel {channel.Number}; offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'" ;
}
default :
if ( channel . FFmpegProfile . Transcode )
{
return _f fmpegProcessService . 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 < Either < BaseError , PlayoutItemWithPath > > ValidatePlayoutItemPath ( PlayoutItem playoutItem )
{
string path = await GetPlayoutItemPath ( playoutItem ) ;
// TODO: this won't work with url streaming from plex
if ( _l ocalFileSystem . FileExists ( path ) )
{
return new PlayoutItemWithPath ( playoutItem , path ) ;
}
return new PlayoutItemDoesNotExistOnDisk ( path ) ;
}
private async Task < string > 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 < string > GetReplacementPlexPath ( int libraryPathId , string path )
private async Task < string > GetReplacementPlexPath ( int libraryPathId , string path )
{
{
List < PlexPathReplacement > replacements =
List < PlexPathReplacement > replacements =
@ -105,5 +186,7 @@ namespace ErsatzTV.Application.Streaming.Queries
} ,
} ,
( ) = > path ) ;
( ) = > path ) ;
}
}
private record PlayoutItemWithPath ( PlayoutItem PlayoutItem , string Path ) ;
}
}
}
}