2021-05-20 21:28:18 +02:00
#nullable disable
2019-12-04 21:39:27 +01:00
#pragma warning disable CS1591
2019-01-06 21:50:43 +01:00
using System ;
2018-12-28 15:30:53 +01:00
using System.Collections.Concurrent ;
using System.Collections.Generic ;
2020-03-27 00:10:16 +01:00
using System.Diagnostics ;
2018-12-28 15:30:53 +01:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
2020-08-31 19:39:40 +02:00
using System.Net.Http ;
2018-12-28 15:30:53 +01:00
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using System.Xml ;
2019-01-13 20:22:00 +01:00
using Emby.Server.Implementations.Library ;
2020-07-01 03:44:41 +02:00
using Jellyfin.Data.Enums ;
2020-08-14 02:48:28 +02:00
using Jellyfin.Data.Events ;
2015-07-20 20:32:55 +02:00
using MediaBrowser.Common.Configuration ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Common.Extensions ;
using MediaBrowser.Common.Progress ;
using MediaBrowser.Controller ;
2015-08-22 21:46:55 +02:00
using MediaBrowser.Controller.Configuration ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Controller.Dto ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Entities.TV ;
2015-08-22 21:46:55 +02:00
using MediaBrowser.Controller.Library ;
2015-07-20 20:32:55 +02:00
using MediaBrowser.Controller.LiveTv ;
2015-09-21 18:26:05 +02:00
using MediaBrowser.Controller.MediaEncoding ;
2015-08-22 21:46:55 +02:00
using MediaBrowser.Controller.Providers ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Model.Configuration ;
2015-07-20 20:32:55 +02:00
using MediaBrowser.Model.Dto ;
2015-08-21 21:25:35 +02:00
using MediaBrowser.Model.Entities ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Model.IO ;
2015-07-20 20:32:55 +02:00
using MediaBrowser.Model.LiveTv ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Model.MediaInfo ;
using MediaBrowser.Model.Providers ;
using MediaBrowser.Model.Querying ;
using Microsoft.Extensions.Logging ;
2015-07-20 20:32:55 +02:00
2016-11-04 00:35:19 +01:00
namespace Emby.Server.Implementations.LiveTv.EmbyTV
2015-07-20 20:32:55 +02:00
{
2016-10-05 09:15:29 +02:00
public class EmbyTV : ILiveTvService , ISupportsDirectStreamProvider , ISupportsNewTimerIds , IDisposable
2015-07-20 20:32:55 +02:00
{
2019-12-04 21:39:27 +01:00
public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss" ;
private const int TunerDiscoveryDurationMs = 3000 ;
2016-10-09 09:18:43 +02:00
private readonly IServerApplicationHost _appHost ;
2020-06-06 02:15:56 +02:00
private readonly ILogger < EmbyTV > _logger ;
2020-08-31 19:39:40 +02:00
private readonly IHttpClientFactory _httpClientFactory ;
2015-08-22 21:46:55 +02:00
private readonly IServerConfigurationManager _config ;
2015-07-20 20:32:55 +02:00
private readonly ItemDataProvider < SeriesTimerInfo > _seriesTimerProvider ;
private readonly TimerManager _timerProvider ;
2015-07-23 15:23:22 +02:00
private readonly LiveTvManager _liveTvManager ;
2015-08-16 20:54:25 +02:00
private readonly IFileSystem _fileSystem ;
2015-07-23 07:25:55 +02:00
2015-08-22 21:46:55 +02:00
private readonly ILibraryMonitor _libraryMonitor ;
private readonly ILibraryManager _libraryManager ;
private readonly IProviderManager _providerManager ;
2015-09-21 18:26:05 +02:00
private readonly IMediaEncoder _mediaEncoder ;
2019-12-04 21:39:27 +01:00
private readonly IMediaSourceManager _mediaSourceManager ;
private readonly IStreamHelper _streamHelper ;
2016-05-04 22:50:47 +02:00
private readonly ConcurrentDictionary < string , ActiveRecordingInfo > _activeRecordings =
new ConcurrentDictionary < string , ActiveRecordingInfo > ( StringComparer . OrdinalIgnoreCase ) ;
2019-12-04 21:39:27 +01:00
private readonly ConcurrentDictionary < string , EpgChannelData > _epgChannels =
new ConcurrentDictionary < string , EpgChannelData > ( StringComparer . OrdinalIgnoreCase ) ;
private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim ( 1 , 1 ) ;
2018-09-12 19:26:21 +02:00
2019-12-04 21:39:27 +01:00
private bool _disposed = false ;
public EmbyTV (
IServerApplicationHost appHost ,
2019-01-02 15:57:48 +01:00
IStreamHelper streamHelper ,
IMediaSourceManager mediaSourceManager ,
2020-03-03 23:07:10 +01:00
ILogger < EmbyTV > logger ,
2020-08-31 19:39:40 +02:00
IHttpClientFactory httpClientFactory ,
2019-01-02 15:57:48 +01:00
IServerConfigurationManager config ,
ILiveTvManager liveTvManager ,
IFileSystem fileSystem ,
ILibraryManager libraryManager ,
ILibraryMonitor libraryMonitor ,
IProviderManager providerManager ,
2020-03-27 00:45:48 +01:00
IMediaEncoder mediaEncoder )
2015-07-20 20:32:55 +02:00
{
2015-07-29 05:42:03 +02:00
Current = this ;
2016-10-09 09:18:43 +02:00
_appHost = appHost ;
2015-07-20 20:32:55 +02:00
_logger = logger ;
2020-08-31 19:39:40 +02:00
_httpClientFactory = httpClientFactory ;
2015-07-20 20:32:55 +02:00
_config = config ;
2015-08-16 20:54:25 +02:00
_fileSystem = fileSystem ;
2015-08-22 21:46:55 +02:00
_libraryManager = libraryManager ;
_libraryMonitor = libraryMonitor ;
_providerManager = providerManager ;
2015-09-21 18:26:05 +02:00
_mediaEncoder = mediaEncoder ;
2015-07-23 15:23:22 +02:00
_liveTvManager = ( LiveTvManager ) liveTvManager ;
2018-09-12 19:26:21 +02:00
_mediaSourceManager = mediaSourceManager ;
_streamHelper = streamHelper ;
2015-07-20 20:32:55 +02:00
2020-12-23 13:12:40 +01:00
_seriesTimerProvider = new SeriesTimerManager ( _logger , Path . Combine ( DataPath , "seriestimers.json" ) ) ;
_timerProvider = new TimerManager ( _logger , Path . Combine ( DataPath , "timers.json" ) ) ;
2019-12-04 21:39:27 +01:00
_timerProvider . TimerFired + = OnTimerProviderTimerFired ;
2016-05-04 22:50:47 +02:00
2019-12-04 21:39:27 +01:00
_config . NamedConfigurationUpdated + = OnNamedConfigurationUpdated ;
2016-05-04 22:50:47 +02:00
}
2019-12-04 21:39:27 +01:00
public event EventHandler < GenericEventArgs < TimerInfo > > TimerCreated ;
public event EventHandler < GenericEventArgs < string > > TimerCancelled ;
public static EmbyTV Current { get ; private set ; }
/// <inheritdoc />
public string Name = > "Emby" ;
public string DataPath = > Path . Combine ( _config . CommonApplicationPaths . DataPath , "livetv" ) ;
/// <inheritdoc />
public string HomePageUrl = > "https://github.com/jellyfin/jellyfin" ;
private string DefaultRecordingPath = > Path . Combine ( DataPath , "recordings" ) ;
private string RecordingPath
{
get
{
var path = GetConfiguration ( ) . RecordingPath ;
return string . IsNullOrWhiteSpace ( path )
? DefaultRecordingPath
: path ;
}
}
2020-05-25 23:52:51 +02:00
private async void OnNamedConfigurationUpdated ( object sender , ConfigurationUpdateEventArgs e )
2016-05-04 22:50:47 +02:00
{
if ( string . Equals ( e . Key , "livetv" , StringComparison . OrdinalIgnoreCase ) )
{
2020-05-25 23:52:51 +02:00
await CreateRecordingFolders ( ) . ConfigureAwait ( false ) ;
2016-05-04 22:50:47 +02:00
}
2015-07-20 20:32:55 +02:00
}
2019-12-04 21:39:27 +01:00
public Task Start ( )
2015-07-29 05:42:03 +02:00
{
_timerProvider . RestartTimers ( ) ;
2016-01-27 07:07:01 +01:00
2019-12-04 21:39:27 +01:00
return CreateRecordingFolders ( ) ;
2016-05-04 22:50:47 +02:00
}
2018-09-12 19:26:21 +02:00
internal async Task CreateRecordingFolders ( )
2016-05-22 19:07:30 +02:00
{
try
{
2019-12-04 21:39:27 +01:00
var recordingFolders = GetRecordingFolders ( ) . ToArray ( ) ;
2018-09-12 19:26:21 +02:00
var virtualFolders = _libraryManager . GetVirtualFolders ( )
. ToList ( ) ;
2016-05-04 22:50:47 +02:00
2018-09-12 19:26:21 +02:00
var allExistingPaths = virtualFolders . SelectMany ( i = > i . Locations ) . ToList ( ) ;
2016-05-04 22:50:47 +02:00
2018-09-12 19:26:21 +02:00
var pathsAdded = new List < string > ( ) ;
2016-05-04 22:50:47 +02:00
2018-09-12 19:26:21 +02:00
foreach ( var recordingFolder in recordingFolders )
{
var pathsToCreate = recordingFolder . Locations
. Where ( i = > ! allExistingPaths . Any ( p = > _fileSystem . AreEqual ( p , i ) ) )
. ToList ( ) ;
2016-05-20 17:57:07 +02:00
2018-09-12 19:26:21 +02:00
if ( pathsToCreate . Count = = 0 )
{
continue ;
}
2016-05-04 22:50:47 +02:00
2018-09-12 19:26:21 +02:00
var mediaPathInfos = pathsToCreate . Select ( i = > new MediaPathInfo { Path = i } ) . ToArray ( ) ;
2016-05-04 22:50:47 +02:00
2018-09-12 19:26:21 +02:00
var libraryOptions = new LibraryOptions
{
PathInfos = mediaPathInfos
} ;
try
{
await _libraryManager . AddVirtualFolder ( recordingFolder . Name , recordingFolder . CollectionType , libraryOptions , true ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error creating virtual folder" ) ;
2018-09-12 19:26:21 +02:00
}
2016-09-23 08:21:54 +02:00
2018-09-12 19:26:21 +02:00
pathsAdded . AddRange ( pathsToCreate ) ;
2016-05-04 22:50:47 +02:00
}
2016-05-20 17:57:07 +02:00
2018-09-12 19:26:21 +02:00
var config = GetConfiguration ( ) ;
2016-05-20 17:57:07 +02:00
2018-09-12 19:26:21 +02:00
var pathsToRemove = config . MediaLocationsCreated
. Except ( recordingFolders . SelectMany ( i = > i . Locations ) )
. ToList ( ) ;
2016-05-20 17:57:07 +02:00
2018-09-12 19:26:21 +02:00
if ( pathsAdded . Count > 0 | | pathsToRemove . Count > 0 )
{
pathsAdded . InsertRange ( 0 , config . MediaLocationsCreated ) ;
config . MediaLocationsCreated = pathsAdded . Except ( pathsToRemove ) . Distinct ( StringComparer . OrdinalIgnoreCase ) . ToArray ( ) ;
_config . SaveConfiguration ( "livetv" , config ) ;
}
2016-05-20 17:57:07 +02:00
2018-09-12 19:26:21 +02:00
foreach ( var path in pathsToRemove )
{
await RemovePathFromLibrary ( path ) . ConfigureAwait ( false ) ;
}
2016-05-20 17:57:07 +02:00
}
2018-09-12 19:26:21 +02:00
catch ( Exception ex )
2016-05-20 17:57:07 +02:00
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error creating recording folders" ) ;
2016-05-04 22:50:47 +02:00
}
}
2018-09-12 19:26:21 +02:00
private async Task RemovePathFromLibrary ( string path )
2016-05-04 22:50:47 +02:00
{
2018-12-13 14:18:25 +01:00
_logger . LogDebug ( "Removing path from library: {0}" , path ) ;
2016-05-20 17:57:07 +02:00
2016-05-04 22:50:47 +02:00
var requiresRefresh = false ;
var virtualFolders = _libraryManager . GetVirtualFolders ( )
. ToList ( ) ;
foreach ( var virtualFolder in virtualFolders )
{
if ( ! virtualFolder . Locations . Contains ( path , StringComparer . OrdinalIgnoreCase ) )
{
continue ;
}
2017-08-19 21:43:35 +02:00
if ( virtualFolder . Locations . Length = = 1 )
2016-05-04 22:50:47 +02:00
{
// remove entire virtual folder
try
{
2018-09-12 19:26:21 +02:00
await _libraryManager . RemoveVirtualFolder ( virtualFolder . Name , true ) . ConfigureAwait ( false ) ;
2016-05-04 22:50:47 +02:00
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error removing virtual folder" ) ;
2016-05-04 22:50:47 +02:00
}
}
else
{
try
{
_libraryManager . RemoveMediaPath ( virtualFolder . Name , path ) ;
requiresRefresh = true ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error removing media path" ) ;
2016-05-04 22:50:47 +02:00
}
}
}
if ( requiresRefresh )
{
2019-10-25 12:47:20 +02:00
await _libraryManager . ValidateMediaLibrary ( new SimpleProgress < double > ( ) , CancellationToken . None ) . ConfigureAwait ( false ) ;
2016-05-04 22:50:47 +02:00
}
2016-01-27 07:07:01 +01:00
}
2019-03-25 22:25:32 +01:00
public async Task RefreshSeriesTimers ( CancellationToken cancellationToken )
2015-09-21 18:26:05 +02:00
{
2015-11-21 05:34:55 +01:00
var seriesTimers = await GetSeriesTimersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2015-09-21 18:26:05 +02:00
2015-11-21 05:34:55 +01:00
foreach ( var timer in seriesTimers )
2015-09-21 18:26:05 +02:00
{
2019-02-01 21:56:50 +01:00
UpdateTimersForSeriesTimer ( timer , false , true ) ;
2015-09-21 18:26:05 +02:00
}
2016-12-13 19:23:03 +01:00
}
2015-11-21 05:34:55 +01:00
2019-03-25 22:25:32 +01:00
public async Task RefreshTimers ( CancellationToken cancellationToken )
2016-12-13 19:23:03 +01:00
{
2015-11-21 05:34:55 +01:00
var timers = await GetTimersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2018-09-12 19:26:21 +02:00
var tempChannelCache = new Dictionary < Guid , LiveTvChannel > ( ) ;
2017-09-08 18:13:58 +02:00
2016-12-13 19:23:03 +01:00
foreach ( var timer in timers )
2015-11-21 05:34:55 +01:00
{
if ( DateTime . UtcNow > timer . EndDate & & ! _activeRecordings . ContainsKey ( timer . Id ) )
{
2016-09-26 20:59:18 +02:00
OnTimerOutOfDate ( timer ) ;
2016-12-13 19:23:03 +01:00
continue ;
}
if ( string . IsNullOrWhiteSpace ( timer . ProgramId ) | | string . IsNullOrWhiteSpace ( timer . ChannelId ) )
{
continue ;
2015-11-21 05:34:55 +01:00
}
2016-12-13 19:23:03 +01:00
2017-09-08 18:13:58 +02:00
var program = GetProgramInfoFromCache ( timer ) ;
2016-12-13 19:23:03 +01:00
if ( program = = null )
{
OnTimerOutOfDate ( timer ) ;
continue ;
}
2017-09-08 18:13:58 +02:00
CopyProgramInfoToTimerInfo ( program , timer , tempChannelCache ) ;
2016-12-13 19:23:03 +01:00
_timerProvider . Update ( timer ) ;
2015-11-21 05:34:55 +01:00
}
2015-09-21 18:26:05 +02:00
}
2016-09-26 20:59:18 +02:00
private void OnTimerOutOfDate ( TimerInfo timer )
{
_timerProvider . Delete ( timer ) ;
}
2015-08-16 20:54:25 +02:00
private async Task < IEnumerable < ChannelInfo > > GetChannelsAsync ( bool enableCache , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
var list = new List < ChannelInfo > ( ) ;
2015-08-19 18:43:23 +02:00
foreach ( var hostInstance in _liveTvManager . TunerHosts )
2015-07-20 20:32:55 +02:00
{
2015-07-24 01:40:54 +02:00
try
2015-07-20 20:32:55 +02:00
{
2016-09-29 14:55:49 +02:00
var channels = await hostInstance . GetChannels ( enableCache , cancellationToken ) . ConfigureAwait ( false ) ;
2015-07-20 20:32:55 +02:00
2015-08-16 20:37:53 +02:00
list . AddRange ( channels ) ;
2015-07-24 01:40:54 +02:00
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error getting channels" ) ;
2015-07-20 20:32:55 +02:00
}
}
2016-02-24 20:06:26 +01:00
foreach ( var provider in GetListingProviders ( ) )
2015-07-23 07:25:55 +02:00
{
2016-02-24 20:06:26 +01:00
var enabledChannels = list
. Where ( i = > IsListingProviderEnabledForTuner ( provider . Item2 , i . TunerHostId ) )
. ToList ( ) ;
if ( enabledChannels . Count > 0 )
2015-07-23 07:25:55 +02:00
{
2015-08-03 01:47:31 +02:00
try
{
2017-02-05 00:32:16 +01:00
await AddMetadata ( provider . Item1 , provider . Item2 , enabledChannels , enableCache , cancellationToken ) . ConfigureAwait ( false ) ;
2015-08-03 01:47:31 +02:00
}
catch ( NotSupportedException )
{
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error adding metadata" ) ;
2015-08-03 01:47:31 +02:00
}
2015-07-23 07:25:55 +02:00
}
}
2016-02-24 20:06:26 +01:00
2015-07-20 20:32:55 +02:00
return list ;
}
2019-12-04 21:39:27 +01:00
private async Task AddMetadata (
IListingsProvider provider ,
ListingsProviderInfo info ,
IEnumerable < ChannelInfo > tunerChannels ,
bool enableCache ,
CancellationToken cancellationToken )
2017-02-05 00:32:16 +01:00
{
var epgChannels = await GetEpgChannels ( provider , info , enableCache , cancellationToken ) . ConfigureAwait ( false ) ;
foreach ( var tunerChannel in tunerChannels )
{
var epgChannel = GetEpgChannelFromTunerChannel ( info , tunerChannel , epgChannels ) ;
if ( epgChannel ! = null )
{
if ( ! string . IsNullOrWhiteSpace ( epgChannel . Name ) )
{
2019-12-04 21:39:27 +01:00
// tunerChannel.Name = epgChannel.Name;
2017-02-05 00:32:16 +01:00
}
2019-12-04 21:39:27 +01:00
2017-02-10 01:15:07 +01:00
if ( ! string . IsNullOrWhiteSpace ( epgChannel . ImageUrl ) )
{
tunerChannel . ImageUrl = epgChannel . ImageUrl ;
}
2017-02-05 00:32:16 +01:00
}
}
}
2019-12-04 21:39:27 +01:00
private async Task < EpgChannelData > GetEpgChannels (
IListingsProvider provider ,
ListingsProviderInfo info ,
bool enableCache ,
CancellationToken cancellationToken )
2017-02-05 00:32:16 +01:00
{
2019-01-13 21:46:33 +01:00
if ( ! enableCache | | ! _epgChannels . TryGetValue ( info . Id , out var result ) )
2017-02-05 00:32:16 +01:00
{
2018-09-12 19:26:21 +02:00
var channels = await provider . GetChannels ( info , cancellationToken ) . ConfigureAwait ( false ) ;
2017-02-05 00:32:16 +01:00
2018-09-12 19:26:21 +02:00
foreach ( var channel in channels )
2017-03-25 23:08:43 +01:00
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Found epg channel in {0} {1} {2} {3}" , provider . Name , info . ListingsId , channel . Name , channel . Id ) ;
2017-03-25 23:08:43 +01:00
}
2018-09-12 19:26:21 +02:00
result = new EpgChannelData ( channels ) ;
2017-02-05 00:32:16 +01:00
_epgChannels . AddOrUpdate ( info . Id , result , ( k , v ) = > result ) ;
}
return result ;
}
private async Task < ChannelInfo > GetEpgChannelFromTunerChannel ( IListingsProvider provider , ListingsProviderInfo info , ChannelInfo tunerChannel , CancellationToken cancellationToken )
{
var epgChannels = await GetEpgChannels ( provider , info , true , cancellationToken ) . ConfigureAwait ( false ) ;
return GetEpgChannelFromTunerChannel ( info , tunerChannel , epgChannels ) ;
}
2019-01-06 21:50:43 +01:00
private static string GetMappedChannel ( string channelId , NameValuePair [ ] mappings )
2017-02-05 00:32:16 +01:00
{
2019-01-17 18:47:41 +01:00
foreach ( NameValuePair mapping in mappings )
2017-02-05 00:32:16 +01:00
{
2020-01-09 17:07:13 +01:00
if ( string . Equals ( mapping . Name , channelId , StringComparison . OrdinalIgnoreCase ) )
2017-02-05 00:32:16 +01:00
{
return mapping . Value ;
}
}
2019-12-04 21:39:27 +01:00
2017-02-05 00:32:16 +01:00
return channelId ;
}
2018-09-12 19:26:21 +02:00
internal ChannelInfo GetEpgChannelFromTunerChannel ( NameValuePair [ ] mappings , ChannelInfo tunerChannel , List < ChannelInfo > epgChannels )
{
return GetEpgChannelFromTunerChannel ( mappings , tunerChannel , new EpgChannelData ( epgChannels ) ) ;
}
private ChannelInfo GetEpgChannelFromTunerChannel ( ListingsProviderInfo info , ChannelInfo tunerChannel , EpgChannelData epgChannels )
2017-02-05 00:32:16 +01:00
{
2017-08-19 21:43:35 +02:00
return GetEpgChannelFromTunerChannel ( info . ChannelMappings , tunerChannel , epgChannels ) ;
2017-02-05 00:32:16 +01:00
}
2019-12-04 21:39:27 +01:00
private ChannelInfo GetEpgChannelFromTunerChannel (
NameValuePair [ ] mappings ,
ChannelInfo tunerChannel ,
EpgChannelData epgChannelData )
2017-02-05 00:32:16 +01:00
{
2017-02-23 20:13:26 +01:00
if ( ! string . IsNullOrWhiteSpace ( tunerChannel . Id ) )
{
var mappedTunerChannelId = GetMappedChannel ( tunerChannel . Id , mappings ) ;
if ( string . IsNullOrWhiteSpace ( mappedTunerChannelId ) )
{
mappedTunerChannelId = tunerChannel . Id ;
}
2018-09-12 19:26:21 +02:00
var channel = epgChannelData . GetChannelById ( mappedTunerChannelId ) ;
2017-02-23 20:13:26 +01:00
if ( channel ! = null )
{
return channel ;
}
}
2017-02-08 22:29:08 +01:00
2017-02-23 20:13:26 +01:00
if ( ! string . IsNullOrWhiteSpace ( tunerChannel . TunerChannelId ) )
2017-02-05 00:32:16 +01:00
{
2017-03-26 06:21:32 +02:00
var tunerChannelId = tunerChannel . TunerChannelId ;
if ( tunerChannelId . IndexOf ( ".json.schedulesdirect.org" , StringComparison . OrdinalIgnoreCase ) ! = - 1 )
{
tunerChannelId = tunerChannelId . Replace ( ".json.schedulesdirect.org" , string . Empty , StringComparison . OrdinalIgnoreCase ) . TrimStart ( 'I' ) ;
}
var mappedTunerChannelId = GetMappedChannel ( tunerChannelId , mappings ) ;
2017-02-05 00:32:16 +01:00
2017-02-08 22:29:08 +01:00
if ( string . IsNullOrWhiteSpace ( mappedTunerChannelId ) )
2017-02-05 00:32:16 +01:00
{
2017-03-26 06:21:32 +02:00
mappedTunerChannelId = tunerChannelId ;
2017-02-05 00:32:16 +01:00
}
2018-09-12 19:26:21 +02:00
var channel = epgChannelData . GetChannelById ( mappedTunerChannelId ) ;
2017-02-05 00:32:16 +01:00
if ( channel ! = null )
{
return channel ;
}
}
if ( ! string . IsNullOrWhiteSpace ( tunerChannel . Number ) )
{
var tunerChannelNumber = GetMappedChannel ( tunerChannel . Number , mappings ) ;
if ( string . IsNullOrWhiteSpace ( tunerChannelNumber ) )
{
tunerChannelNumber = tunerChannel . Number ;
}
2018-09-12 19:26:21 +02:00
var channel = epgChannelData . GetChannelByNumber ( tunerChannelNumber ) ;
2017-02-05 00:32:16 +01:00
if ( channel ! = null )
{
return channel ;
}
}
if ( ! string . IsNullOrWhiteSpace ( tunerChannel . Name ) )
{
2019-12-04 21:39:27 +01:00
var normalizedName = EpgChannelData . NormalizeName ( tunerChannel . Name ) ;
2017-02-05 00:32:16 +01:00
2018-09-12 19:26:21 +02:00
var channel = epgChannelData . GetChannelByName ( normalizedName ) ;
2017-02-05 00:32:16 +01:00
if ( channel ! = null )
{
return channel ;
}
}
return null ;
}
2016-06-09 18:13:25 +02:00
public async Task < List < ChannelInfo > > GetChannelsForListingsProvider ( ListingsProviderInfo listingsProvider , CancellationToken cancellationToken )
{
var list = new List < ChannelInfo > ( ) ;
foreach ( var hostInstance in _liveTvManager . TunerHosts )
{
try
{
2016-09-29 14:55:49 +02:00
var channels = await hostInstance . GetChannels ( false , cancellationToken ) . ConfigureAwait ( false ) ;
2016-06-09 18:13:25 +02:00
list . AddRange ( channels ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error getting channels" ) ;
2016-06-09 18:13:25 +02:00
}
}
return list
. Where ( i = > IsListingProviderEnabledForTuner ( listingsProvider , i . TunerHostId ) )
. ToList ( ) ;
}
2015-08-16 20:54:25 +02:00
public Task < IEnumerable < ChannelInfo > > GetChannelsAsync ( CancellationToken cancellationToken )
{
return GetChannelsAsync ( false , cancellationToken ) ;
}
2015-07-20 20:32:55 +02:00
public Task CancelSeriesTimerAsync ( string timerId , CancellationToken cancellationToken )
{
2016-02-01 01:57:40 +01:00
var timers = _timerProvider
. GetAll ( )
. Where ( i = > string . Equals ( i . SeriesTimerId , timerId , StringComparison . OrdinalIgnoreCase ) )
. ToList ( ) ;
2015-08-17 18:52:56 +02:00
foreach ( var timer in timers )
{
2018-09-12 19:26:21 +02:00
CancelTimerInternal ( timer . Id , true , true ) ;
2015-08-17 18:52:56 +02:00
}
2015-07-29 05:42:03 +02:00
var remove = _seriesTimerProvider . GetAll ( ) . FirstOrDefault ( r = > string . Equals ( r . Id , timerId , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-20 20:32:55 +02:00
if ( remove ! = null )
{
_seriesTimerProvider . Delete ( remove ) ;
}
2019-12-04 21:39:27 +01:00
2018-09-12 19:26:21 +02:00
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
2018-09-12 19:26:21 +02:00
private void CancelTimerInternal ( string timerId , bool isSeriesCancelled , bool isManualCancellation )
2015-07-20 20:32:55 +02:00
{
2016-09-27 07:13:56 +02:00
var timer = _timerProvider . GetTimer ( timerId ) ;
2016-09-26 20:59:18 +02:00
if ( timer ! = null )
2015-07-20 20:32:55 +02:00
{
2018-09-12 19:26:21 +02:00
var statusChanging = timer . Status ! = RecordingStatus . Cancelled ;
2017-08-24 21:52:19 +02:00
timer . Status = RecordingStatus . Cancelled ;
2018-09-12 19:26:21 +02:00
if ( isManualCancellation )
{
timer . IsManual = true ;
}
2016-09-26 20:59:18 +02:00
if ( string . IsNullOrWhiteSpace ( timer . SeriesTimerId ) | | isSeriesCancelled )
{
_timerProvider . Delete ( timer ) ;
}
else
{
_timerProvider . AddOrUpdate ( timer , false ) ;
}
2018-09-12 19:26:21 +02:00
if ( statusChanging & & TimerCancelled ! = null )
{
TimerCancelled ( this , new GenericEventArgs < string > ( timerId ) ) ;
}
2015-07-20 20:32:55 +02:00
}
2019-01-13 21:46:33 +01:00
if ( _activeRecordings . TryGetValue ( timerId , out var activeRecordingInfo ) )
2017-09-08 18:13:58 +02:00
{
2017-08-24 21:52:19 +02:00
activeRecordingInfo . Timer = timer ;
2016-03-01 05:24:42 +01:00
activeRecordingInfo . CancellationTokenSource . Cancel ( ) ;
2015-07-20 20:32:55 +02:00
}
}
public Task CancelTimerAsync ( string timerId , CancellationToken cancellationToken )
{
2018-09-12 19:26:21 +02:00
CancelTimerInternal ( timerId , false , true ) ;
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
2016-09-27 07:13:56 +02:00
public Task CreateSeriesTimerAsync ( SeriesTimerInfo info , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
2015-07-20 20:32:55 +02:00
public Task CreateTimerAsync ( TimerInfo info , CancellationToken cancellationToken )
2016-09-27 07:13:56 +02:00
{
throw new NotImplementedException ( ) ;
}
public Task < string > CreateTimer ( TimerInfo timer , CancellationToken cancellationToken )
2016-06-08 23:04:52 +02:00
{
2017-03-26 06:21:32 +02:00
var existingTimer = string . IsNullOrWhiteSpace ( timer . ProgramId ) ?
null :
_timerProvider . GetTimerByProgramId ( timer . ProgramId ) ;
2016-09-26 20:59:18 +02:00
if ( existingTimer ! = null )
{
2016-09-30 08:50:06 +02:00
if ( existingTimer . Status = = RecordingStatus . Cancelled | |
existingTimer . Status = = RecordingStatus . Completed )
2016-09-26 20:59:18 +02:00
{
existingTimer . Status = RecordingStatus . New ;
2017-02-20 08:04:03 +01:00
existingTimer . IsManual = true ;
2016-09-26 20:59:18 +02:00
_timerProvider . Update ( existingTimer ) ;
2016-09-27 07:13:56 +02:00
return Task . FromResult ( existingTimer . Id ) ;
2016-09-26 20:59:18 +02:00
}
else
{
throw new ArgumentException ( "A scheduled recording already exists for this program." ) ;
}
}
2019-02-28 23:22:57 +01:00
timer . Id = Guid . NewGuid ( ) . ToString ( "N" , CultureInfo . InvariantCulture ) ;
2016-09-15 08:23:39 +02:00
2017-09-08 18:13:58 +02:00
LiveTvProgram programInfo = null ;
2016-09-15 08:23:39 +02:00
if ( ! string . IsNullOrWhiteSpace ( timer . ProgramId ) )
{
2017-09-08 18:13:58 +02:00
programInfo = GetProgramInfoFromCache ( timer ) ;
2016-09-15 08:23:39 +02:00
}
2019-12-04 21:39:27 +01:00
2016-09-15 08:23:39 +02:00
if ( programInfo = = null )
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Unable to find program with Id {0}. Will search using start date" , timer . ProgramId ) ;
2016-09-15 08:23:39 +02:00
programInfo = GetProgramInfoFromCache ( timer . ChannelId , timer . StartDate ) ;
}
if ( programInfo ! = null )
{
2017-09-08 18:13:58 +02:00
CopyProgramInfoToTimerInfo ( programInfo , timer ) ;
2016-09-15 08:23:39 +02:00
}
2017-02-20 08:04:03 +01:00
timer . IsManual = true ;
2016-09-15 08:23:39 +02:00
_timerProvider . Add ( timer ) ;
2018-09-12 19:26:21 +02:00
2019-12-04 21:39:27 +01:00
TimerCreated ? . Invoke ( this , new GenericEventArgs < TimerInfo > ( timer ) ) ;
2018-09-12 19:26:21 +02:00
2016-09-15 08:23:39 +02:00
return Task . FromResult ( timer . Id ) ;
2015-07-20 20:32:55 +02:00
}
2016-06-08 23:04:52 +02:00
public async Task < string > CreateSeriesTimer ( SeriesTimerInfo info , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
2019-02-28 23:22:57 +01:00
info . Id = Guid . NewGuid ( ) . ToString ( "N" , CultureInfo . InvariantCulture ) ;
2015-07-20 20:32:55 +02:00
2015-08-17 00:03:22 +02:00
// populate info.seriesID
2017-09-08 18:13:58 +02:00
var program = GetProgramInfoFromCache ( info . ProgramId ) ;
2015-08-17 00:03:22 +02:00
if ( program ! = null )
{
2017-09-08 18:13:58 +02:00
info . SeriesId = program . ExternalSeriesId ;
2015-08-17 00:03:22 +02:00
}
else
{
throw new InvalidOperationException ( "SeriesId for program not found" ) ;
}
2017-03-24 16:03:49 +01:00
// If any timers have already been manually created, make sure they don't get cancelled
var existingTimers = ( await GetTimersAsync ( CancellationToken . None ) . ConfigureAwait ( false ) )
. Where ( i = >
{
if ( string . Equals ( i . ProgramId , info . ProgramId , StringComparison . OrdinalIgnoreCase ) & & ! string . IsNullOrWhiteSpace ( info . ProgramId ) )
{
return true ;
}
2017-03-26 06:21:32 +02:00
if ( string . Equals ( i . SeriesId , info . SeriesId , StringComparison . OrdinalIgnoreCase ) & & ! string . IsNullOrWhiteSpace ( info . SeriesId ) )
{
return true ;
}
2017-03-24 16:03:49 +01:00
return false ;
} )
. ToList ( ) ;
2015-07-20 20:32:55 +02:00
_seriesTimerProvider . Add ( info ) ;
2017-03-24 16:03:49 +01:00
foreach ( var timer in existingTimers )
{
timer . SeriesTimerId = info . Id ;
timer . IsManual = true ;
2017-03-26 06:21:32 +02:00
_timerProvider . AddOrUpdate ( timer , false ) ;
2017-03-24 16:03:49 +01:00
}
2019-02-01 21:56:50 +01:00
UpdateTimersForSeriesTimer ( info , true , false ) ;
2016-06-08 23:04:52 +02:00
return info . Id ;
2015-07-20 20:32:55 +02:00
}
2019-02-01 21:56:50 +01:00
public Task UpdateSeriesTimerAsync ( SeriesTimerInfo info , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
2016-01-20 04:48:37 +01:00
var instance = _seriesTimerProvider . GetAll ( ) . FirstOrDefault ( i = > string . Equals ( i . Id , info . Id , StringComparison . OrdinalIgnoreCase ) ) ;
if ( instance ! = null )
2015-08-17 00:03:22 +02:00
{
2016-01-20 04:48:37 +01:00
instance . ChannelId = info . ChannelId ;
instance . Days = info . Days ;
instance . EndDate = info . EndDate ;
instance . IsPostPaddingRequired = info . IsPostPaddingRequired ;
instance . IsPrePaddingRequired = info . IsPrePaddingRequired ;
instance . PostPaddingSeconds = info . PostPaddingSeconds ;
instance . PrePaddingSeconds = info . PrePaddingSeconds ;
instance . Priority = info . Priority ;
instance . RecordAnyChannel = info . RecordAnyChannel ;
instance . RecordAnyTime = info . RecordAnyTime ;
instance . RecordNewOnly = info . RecordNewOnly ;
2016-09-21 23:09:14 +02:00
instance . SkipEpisodesInLibrary = info . SkipEpisodesInLibrary ;
instance . KeepUpTo = info . KeepUpTo ;
2016-09-26 20:59:18 +02:00
instance . KeepUntil = info . KeepUntil ;
2016-01-20 04:48:37 +01:00
instance . StartDate = info . StartDate ;
_seriesTimerProvider . Update ( instance ) ;
2015-08-17 00:03:22 +02:00
2019-02-01 21:56:50 +01:00
UpdateTimersForSeriesTimer ( instance , true , true ) ;
2016-01-20 04:48:37 +01:00
}
2019-02-01 21:56:50 +01:00
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
2016-09-26 20:59:18 +02:00
public Task UpdateTimerAsync ( TimerInfo updatedTimer , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
2016-09-27 07:13:56 +02:00
var existingTimer = _timerProvider . GetTimer ( updatedTimer . Id ) ;
2016-09-26 20:59:18 +02:00
if ( existingTimer = = null )
{
throw new ResourceNotFoundException ( ) ;
}
// Only update if not currently active
2019-12-04 21:39:27 +01:00
if ( ! _activeRecordings . TryGetValue ( updatedTimer . Id , out _ ) )
2016-09-26 20:59:18 +02:00
{
2016-10-03 08:28:45 +02:00
existingTimer . PrePaddingSeconds = updatedTimer . PrePaddingSeconds ;
existingTimer . PostPaddingSeconds = updatedTimer . PostPaddingSeconds ;
existingTimer . IsPostPaddingRequired = updatedTimer . IsPostPaddingRequired ;
existingTimer . IsPrePaddingRequired = updatedTimer . IsPrePaddingRequired ;
2017-02-20 08:04:03 +01:00
_timerProvider . Update ( existingTimer ) ;
2016-09-26 20:59:18 +02:00
}
2018-09-12 19:26:21 +02:00
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
2019-01-06 21:50:43 +01:00
private static void UpdateExistingTimerWithNewMetadata ( TimerInfo existingTimer , TimerInfo updatedTimer )
2016-09-26 20:59:18 +02:00
{
// Update the program info but retain the status
existingTimer . ChannelId = updatedTimer . ChannelId ;
existingTimer . CommunityRating = updatedTimer . CommunityRating ;
existingTimer . EndDate = updatedTimer . EndDate ;
existingTimer . EpisodeNumber = updatedTimer . EpisodeNumber ;
existingTimer . EpisodeTitle = updatedTimer . EpisodeTitle ;
existingTimer . Genres = updatedTimer . Genres ;
existingTimer . IsMovie = updatedTimer . IsMovie ;
2017-08-23 21:45:52 +02:00
existingTimer . IsSeries = updatedTimer . IsSeries ;
2018-09-12 19:26:21 +02:00
existingTimer . Tags = updatedTimer . Tags ;
2016-09-26 20:59:18 +02:00
existingTimer . IsProgramSeries = updatedTimer . IsProgramSeries ;
2016-10-04 07:15:39 +02:00
existingTimer . IsRepeat = updatedTimer . IsRepeat ;
2016-09-26 20:59:18 +02:00
existingTimer . Name = updatedTimer . Name ;
existingTimer . OfficialRating = updatedTimer . OfficialRating ;
existingTimer . OriginalAirDate = updatedTimer . OriginalAirDate ;
existingTimer . Overview = updatedTimer . Overview ;
existingTimer . ProductionYear = updatedTimer . ProductionYear ;
existingTimer . ProgramId = updatedTimer . ProgramId ;
existingTimer . SeasonNumber = updatedTimer . SeasonNumber ;
existingTimer . StartDate = updatedTimer . StartDate ;
2016-11-24 17:29:23 +01:00
existingTimer . ShowId = updatedTimer . ShowId ;
2018-09-12 19:26:21 +02:00
existingTimer . ProviderIds = updatedTimer . ProviderIds ;
existingTimer . SeriesProviderIds = updatedTimer . SeriesProviderIds ;
2016-10-09 09:18:43 +02:00
}
public string GetActiveRecordingPath ( string id )
{
2019-01-13 21:46:33 +01:00
if ( _activeRecordings . TryGetValue ( id , out var info ) )
2016-10-09 09:18:43 +02:00
{
return info . Path ;
}
2019-12-04 21:39:27 +01:00
2016-10-09 09:18:43 +02:00
return null ;
}
2017-08-23 21:45:52 +02:00
public ActiveRecordingInfo GetActiveRecordingInfo ( string path )
{
2021-04-30 15:09:36 +02:00
if ( string . IsNullOrWhiteSpace ( path ) | | _activeRecordings . IsEmpty )
2017-08-23 21:45:52 +02:00
{
return null ;
}
2021-04-30 15:09:36 +02:00
foreach ( var ( _ , recordingInfo ) in _activeRecordings )
2017-08-23 21:45:52 +02:00
{
2021-04-30 15:09:36 +02:00
if ( string . Equals ( recordingInfo . Path , path , StringComparison . Ordinal ) & & ! recordingInfo . CancellationTokenSource . IsCancellationRequested )
2017-08-23 21:45:52 +02:00
{
2021-04-30 15:09:36 +02:00
var timer = recordingInfo . Timer ;
2017-08-24 21:52:19 +02:00
if ( timer . Status ! = RecordingStatus . InProgress )
{
return null ;
}
2019-12-04 21:39:27 +01:00
2021-04-30 15:09:36 +02:00
return recordingInfo ;
2017-08-23 21:45:52 +02:00
}
}
2019-12-04 21:39:27 +01:00
2017-08-23 21:45:52 +02:00
return null ;
}
2015-07-20 20:32:55 +02:00
public Task < IEnumerable < TimerInfo > > GetTimersAsync ( CancellationToken cancellationToken )
{
2016-09-26 20:59:18 +02:00
var excludeStatues = new List < RecordingStatus >
{
2016-10-04 07:15:39 +02:00
RecordingStatus . Completed
2016-09-26 20:59:18 +02:00
} ;
var timers = _timerProvider . GetAll ( )
. Where ( i = > ! excludeStatues . Contains ( i . Status ) ) ;
return Task . FromResult ( timers ) ;
2015-07-20 20:32:55 +02:00
}
public Task < SeriesTimerInfo > GetNewTimerDefaultsAsync ( CancellationToken cancellationToken , ProgramInfo program = null )
{
2015-08-24 04:08:20 +02:00
var config = GetConfiguration ( ) ;
2015-07-20 20:32:55 +02:00
var defaults = new SeriesTimerInfo ( )
{
2015-08-24 04:08:20 +02:00
PostPaddingSeconds = Math . Max ( config . PostPaddingSeconds , 0 ) ,
PrePaddingSeconds = Math . Max ( config . PrePaddingSeconds , 0 ) ,
2016-10-14 18:22:04 +02:00
RecordAnyChannel = false ,
2016-05-29 20:42:39 +02:00
RecordAnyTime = true ,
2016-09-14 18:21:33 +02:00
RecordNewOnly = true ,
2016-05-29 20:42:39 +02:00
Days = new List < DayOfWeek >
{
DayOfWeek . Sunday ,
DayOfWeek . Monday ,
DayOfWeek . Tuesday ,
DayOfWeek . Wednesday ,
DayOfWeek . Thursday ,
DayOfWeek . Friday ,
DayOfWeek . Saturday
}
2015-07-20 20:32:55 +02:00
} ;
2015-08-11 19:47:29 +02:00
if ( program ! = null )
{
defaults . SeriesId = program . SeriesId ;
defaults . ProgramId = program . Id ;
2016-10-13 17:07:21 +02:00
defaults . RecordNewOnly = ! program . IsRepeat ;
2018-09-12 19:26:21 +02:00
defaults . Name = program . Name ;
2015-08-11 19:47:29 +02:00
}
2016-10-16 19:11:32 +02:00
defaults . SkipEpisodesInLibrary = defaults . RecordNewOnly ;
2016-09-26 20:59:18 +02:00
defaults . KeepUntil = KeepUntil . UntilDeleted ;
2015-07-20 20:32:55 +02:00
return Task . FromResult ( defaults ) ;
}
public Task < IEnumerable < SeriesTimerInfo > > GetSeriesTimersAsync ( CancellationToken cancellationToken )
{
return Task . FromResult ( ( IEnumerable < SeriesTimerInfo > ) _seriesTimerProvider . GetAll ( ) ) ;
}
2016-02-24 20:06:26 +01:00
private bool IsListingProviderEnabledForTuner ( ListingsProviderInfo info , string tunerHostId )
{
2016-03-16 05:14:38 +01:00
if ( info . EnableAllTuners )
{
return true ;
}
if ( string . IsNullOrWhiteSpace ( tunerHostId ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( tunerHostId ) ) ;
2016-03-16 05:14:38 +01:00
}
return info . EnabledTuners . Contains ( tunerHostId , StringComparer . OrdinalIgnoreCase ) ;
2016-02-24 20:06:26 +01:00
}
2017-09-08 18:13:58 +02:00
public async Task < IEnumerable < ProgramInfo > > GetProgramsAsync ( string channelId , DateTime startDateUtc , DateTime endDateUtc , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
2015-08-16 20:54:25 +02:00
var channels = await GetChannelsAsync ( true , cancellationToken ) . ConfigureAwait ( false ) ;
var channel = channels . First ( i = > string . Equals ( i . Id , channelId , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-23 07:25:55 +02:00
foreach ( var provider in GetListingProviders ( ) )
{
2016-02-24 20:06:26 +01:00
if ( ! IsListingProviderEnabledForTuner ( provider . Item2 , channel . TunerHostId ) )
{
2018-12-13 14:18:25 +01:00
_logger . LogDebug ( "Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner." , channel . Number , channel . Name , provider . Item1 . Name , provider . Item2 . ListingsId ? ? string . Empty ) ;
2016-02-24 20:06:26 +01:00
continue ;
}
2018-12-13 14:18:25 +01:00
_logger . LogDebug ( "Getting programs for channel {0}-{1} from {2}-{3}" , channel . Number , channel . Name , provider . Item1 . Name , provider . Item2 . ListingsId ? ? string . Empty ) ;
2016-03-23 20:06:36 +01:00
2017-02-05 00:32:16 +01:00
var epgChannel = await GetEpgChannelFromTunerChannel ( provider . Item1 , provider . Item2 , channel , cancellationToken ) . ConfigureAwait ( false ) ;
2017-01-31 22:25:54 +01:00
2017-02-05 00:32:16 +01:00
List < ProgramInfo > programs ;
if ( epgChannel = = null )
2016-06-06 20:22:42 +02:00
{
2018-12-13 14:18:25 +01:00
_logger . LogDebug ( "EPG channel not found for tuner channel {0}-{1} from {2}-{3}" , channel . Number , channel . Name , provider . Item1 . Name , provider . Item2 . ListingsId ? ? string . Empty ) ;
2017-02-05 00:32:16 +01:00
programs = new List < ProgramInfo > ( ) ;
}
else
{
programs = ( await provider . Item1 . GetProgramsAsync ( provider . Item2 , epgChannel . Id , startDateUtc , endDateUtc , cancellationToken )
. ConfigureAwait ( false ) ) . ToList ( ) ;
2016-06-06 20:22:42 +02:00
}
2015-07-23 07:25:55 +02:00
2015-07-24 01:40:54 +02:00
// Replace the value that came from the provider with a normalized value
2017-02-05 00:32:16 +01:00
foreach ( var program in programs )
2015-07-24 01:40:54 +02:00
{
program . ChannelId = channelId ;
2017-02-19 04:46:09 +01:00
2018-09-12 19:26:21 +02:00
program . Id + = "_" + channelId ;
2015-07-24 01:40:54 +02:00
}
2017-02-05 00:32:16 +01:00
if ( programs . Count > 0 )
2015-07-23 07:25:55 +02:00
{
2017-02-05 00:32:16 +01:00
return programs ;
2015-07-23 07:25:55 +02:00
}
}
return new List < ProgramInfo > ( ) ;
}
private List < Tuple < IListingsProvider , ListingsProviderInfo > > GetListingProviders ( )
{
return GetConfiguration ( ) . ListingProviders
. Select ( i = >
{
2015-07-23 15:23:22 +02:00
var provider = _liveTvManager . ListingProviders . FirstOrDefault ( l = > string . Equals ( l . Type , i . Type , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-23 07:25:55 +02:00
return provider = = null ? null : new Tuple < IListingsProvider , ListingsProviderInfo > ( provider , i ) ;
} )
. Where ( i = > i ! = null )
. ToList ( ) ;
2015-07-20 20:32:55 +02:00
}
2018-09-12 19:26:21 +02:00
public Task < MediaSourceInfo > GetChannelStream ( string channelId , string streamId , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
throw new NotImplementedException ( ) ;
}
2018-09-12 19:26:21 +02:00
public async Task < ILiveStream > GetChannelStreamWithDirectStreamProvider ( string channelId , string streamId , List < ILiveStream > currentLiveStreams , CancellationToken cancellationToken )
2016-10-05 09:15:29 +02:00
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Streaming Channel " + channelId ) ;
2016-10-05 09:15:29 +02:00
2019-01-08 00:27:46 +01:00
var result = string . IsNullOrEmpty ( streamId ) ?
2018-09-12 19:26:21 +02:00
null :
currentLiveStreams . FirstOrDefault ( i = > string . Equals ( i . OriginalStreamId , streamId , StringComparison . OrdinalIgnoreCase ) ) ;
2016-10-05 09:15:29 +02:00
2018-09-12 19:26:21 +02:00
if ( result ! = null & & result . EnableStreamSharing )
{
result . ConsumerCount + + ;
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Live stream {0} consumer count is now {1}" , streamId , result . ConsumerCount ) ;
2018-09-12 19:26:21 +02:00
return result ;
}
foreach ( var hostInstance in _liveTvManager . TunerHosts )
{
try
{
result = await hostInstance . GetChannelStream ( channelId , streamId , currentLiveStreams , cancellationToken ) . ConfigureAwait ( false ) ;
var openedMediaSource = result . MediaSource ;
result . OriginalStreamId = streamId ;
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}" , streamId , openedMediaSource . Id , openedMediaSource . LiveStreamId ) ;
2018-09-12 19:26:21 +02:00
return result ;
}
catch ( FileNotFoundException )
{
}
catch ( OperationCanceledException )
{
}
}
2015-07-24 01:40:54 +02:00
2018-09-12 19:26:21 +02:00
throw new Exception ( "Tuner not found." ) ;
2016-09-29 14:55:49 +02:00
}
2015-07-24 01:40:54 +02:00
public async Task < List < MediaSourceInfo > > GetChannelStreamMediaSources ( string channelId , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
2016-11-17 04:58:27 +01:00
if ( string . IsNullOrWhiteSpace ( channelId ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( channelId ) ) ;
2016-11-17 04:58:27 +01:00
}
2015-08-19 21:25:18 +02:00
foreach ( var hostInstance in _liveTvManager . TunerHosts )
2015-07-24 01:40:54 +02:00
{
2015-08-16 20:37:53 +02:00
try
2015-07-24 01:40:54 +02:00
{
2015-08-19 21:25:18 +02:00
var sources = await hostInstance . GetChannelStreamMediaSources ( channelId , cancellationToken ) . ConfigureAwait ( false ) ;
2015-07-24 01:40:54 +02:00
2015-08-16 20:37:53 +02:00
if ( sources . Count > 0 )
{
return sources ;
}
}
catch ( NotImplementedException )
{
2015-07-24 01:40:54 +02:00
}
}
2015-08-27 21:59:42 +02:00
throw new NotImplementedException ( ) ;
2015-07-20 20:32:55 +02:00
}
2017-08-23 21:45:52 +02:00
public async Task < List < MediaSourceInfo > > GetRecordingStreamMediaSources ( ActiveRecordingInfo info , CancellationToken cancellationToken )
{
var stream = new MediaSourceInfo
{
2020-05-10 20:36:11 +02:00
EncoderPath = _appHost . GetLoopbackHttpApiUrl ( ) + "/LiveTv/LiveRecordings/" + info . Id + "/stream" ,
2018-09-12 19:26:21 +02:00
EncoderProtocol = MediaProtocol . Http ,
Path = info . Path ,
Protocol = MediaProtocol . File ,
2017-08-23 21:45:52 +02:00
Id = info . Id ,
SupportsDirectPlay = false ,
SupportsDirectStream = true ,
SupportsTranscoding = true ,
IsInfiniteStream = true ,
RequiresOpening = false ,
RequiresClosing = false ,
BufferMs = 0 ,
IgnoreDts = true ,
IgnoreIndex = true
} ;
2020-12-23 13:12:40 +01:00
await new LiveStreamHelper ( _mediaEncoder , _logger , _config . CommonApplicationPaths )
2019-01-21 20:18:52 +01:00
. AddMediaInfoWithProbe ( stream , false , false , cancellationToken ) . ConfigureAwait ( false ) ;
2017-08-23 21:45:52 +02:00
return new List < MediaSourceInfo >
{
stream
} ;
}
2018-09-12 19:26:21 +02:00
public Task CloseLiveStream ( string id , CancellationToken cancellationToken )
2015-07-20 20:32:55 +02:00
{
2018-09-12 19:26:21 +02:00
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
public Task RecordLiveStream ( string id , CancellationToken cancellationToken )
{
2019-01-21 20:18:52 +01:00
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
public Task ResetTuner ( string id , CancellationToken cancellationToken )
{
2019-01-21 20:18:52 +01:00
return Task . CompletedTask ;
2015-07-20 20:32:55 +02:00
}
2019-12-04 21:39:27 +01:00
private async void OnTimerProviderTimerFired ( object sender , GenericEventArgs < TimerInfo > e )
2015-07-20 20:32:55 +02:00
{
2015-08-22 20:29:12 +02:00
var timer = e . Argument ;
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Recording timer fired for {0}." , timer . Name ) ;
2015-09-21 18:23:20 +02:00
2015-07-20 20:32:55 +02:00
try
{
2015-09-26 04:31:13 +02:00
var recordingEndDate = timer . EndDate . AddSeconds ( timer . PostPaddingSeconds ) ;
if ( recordingEndDate < = DateTime . UtcNow )
{
2018-12-13 14:18:25 +01:00
_logger . LogWarning ( "Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended." , timer . Name , timer . Id ) ;
2016-09-26 20:59:18 +02:00
OnTimerOutOfDate ( timer ) ;
2015-09-26 04:31:13 +02:00
return ;
}
2016-03-01 05:24:42 +01:00
var activeRecordingInfo = new ActiveRecordingInfo
{
CancellationTokenSource = new CancellationTokenSource ( ) ,
2017-08-23 21:45:52 +02:00
Timer = timer ,
Id = timer . Id
2016-03-01 05:24:42 +01:00
} ;
2015-07-20 20:32:55 +02:00
2017-10-23 21:14:11 +02:00
if ( ! _activeRecordings . ContainsKey ( timer . Id ) )
2015-07-20 20:32:55 +02:00
{
2017-10-23 21:14:11 +02:00
await RecordStream ( timer , recordingEndDate , activeRecordingInfo ) . ConfigureAwait ( false ) ;
2015-07-20 20:32:55 +02:00
}
2016-01-26 19:18:54 +01:00
else
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Skipping RecordStream because it's already in progress." ) ;
2016-01-26 19:18:54 +01:00
}
2015-07-20 20:32:55 +02:00
}
catch ( OperationCanceledException )
{
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error recording stream" ) ;
2015-07-20 20:32:55 +02:00
}
}
2018-09-12 19:26:21 +02:00
private string GetRecordingPath ( TimerInfo timer , RemoteSearchResult metadata , out string seriesPath )
2015-07-20 20:32:55 +02:00
{
var recordPath = RecordingPath ;
2016-05-04 22:50:47 +02:00
var config = GetConfiguration ( ) ;
2016-09-16 01:19:27 +02:00
seriesPath = null ;
2015-08-21 01:55:23 +02:00
2016-09-15 08:23:39 +02:00
if ( timer . IsProgramSeries )
2015-08-21 01:55:23 +02:00
{
2016-05-04 22:50:47 +02:00
var customRecordingPath = config . SeriesRecordingPath ;
2016-05-09 05:13:38 +02:00
var allowSubfolder = true ;
if ( ! string . IsNullOrWhiteSpace ( customRecordingPath ) )
{
allowSubfolder = string . Equals ( customRecordingPath , recordPath , StringComparison . OrdinalIgnoreCase ) ;
recordPath = customRecordingPath ;
}
if ( allowSubfolder & & config . EnableRecordingSubfolders )
2016-05-04 22:50:47 +02:00
{
recordPath = Path . Combine ( recordPath , "Series" ) ;
}
2018-09-12 19:26:21 +02:00
// trim trailing period from the folder name
var folderName = _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) . TrimEnd ( '.' ) . Trim ( ) ;
if ( metadata ! = null & & metadata . ProductionYear . HasValue )
{
folderName + = " (" + metadata . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) + ")" ;
}
2016-05-04 22:50:47 +02:00
2016-09-15 08:38:09 +02:00
// Can't use the year here in the folder name because it is the year of the episode, not the series.
recordPath = Path . Combine ( recordPath , folderName ) ;
2016-05-03 06:17:57 +02:00
2016-09-16 01:19:27 +02:00
seriesPath = recordPath ;
2016-09-15 08:23:39 +02:00
if ( timer . SeasonNumber . HasValue )
2016-05-03 06:17:57 +02:00
{
2019-12-04 21:39:27 +01:00
folderName = string . Format (
CultureInfo . InvariantCulture ,
"Season {0}" ,
timer . SeasonNumber . Value ) ;
2016-05-03 06:17:57 +02:00
recordPath = Path . Combine ( recordPath , folderName ) ;
}
2015-08-21 01:55:23 +02:00
}
2016-09-15 08:23:39 +02:00
else if ( timer . IsMovie )
2016-08-31 21:17:11 +02:00
{
var customRecordingPath = config . MovieRecordingPath ;
var allowSubfolder = true ;
if ( ! string . IsNullOrWhiteSpace ( customRecordingPath ) )
{
allowSubfolder = string . Equals ( customRecordingPath , recordPath , StringComparison . OrdinalIgnoreCase ) ;
recordPath = customRecordingPath ;
}
if ( allowSubfolder & & config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Movies" ) ;
}
2016-09-15 08:23:39 +02:00
var folderName = _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ;
if ( timer . ProductionYear . HasValue )
2016-08-31 21:17:11 +02:00
{
2016-09-15 08:23:39 +02:00
folderName + = " (" + timer . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) + ")" ;
2016-08-31 21:17:11 +02:00
}
2018-09-12 19:26:21 +02:00
// trim trailing period from the folder name
folderName = folderName . TrimEnd ( '.' ) . Trim ( ) ;
2016-08-31 21:17:11 +02:00
recordPath = Path . Combine ( recordPath , folderName ) ;
}
2016-09-15 08:23:39 +02:00
else if ( timer . IsKids )
2015-08-21 01:55:23 +02:00
{
2016-05-04 22:50:47 +02:00
if ( config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Kids" ) ;
}
2016-09-15 08:23:39 +02:00
var folderName = _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ;
if ( timer . ProductionYear . HasValue )
2016-05-04 22:50:47 +02:00
{
2016-09-15 08:23:39 +02:00
folderName + = " (" + timer . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) + ")" ;
2016-05-04 22:50:47 +02:00
}
2018-09-12 19:26:21 +02:00
// trim trailing period from the folder name
folderName = folderName . TrimEnd ( '.' ) . Trim ( ) ;
2016-05-04 22:50:47 +02:00
recordPath = Path . Combine ( recordPath , folderName ) ;
2015-08-21 01:55:23 +02:00
}
2016-09-15 08:23:39 +02:00
else if ( timer . IsSports )
2015-08-21 01:55:23 +02:00
{
2016-05-04 22:50:47 +02:00
if ( config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Sports" ) ;
}
2019-12-04 21:39:27 +01:00
2016-09-15 08:23:39 +02:00
recordPath = Path . Combine ( recordPath , _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ) ;
2015-08-21 01:55:23 +02:00
}
2015-07-20 20:32:55 +02:00
else
{
2016-05-04 22:50:47 +02:00
if ( config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Other" ) ;
}
2019-12-04 21:39:27 +01:00
2016-09-15 08:23:39 +02:00
recordPath = Path . Combine ( recordPath , _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ) ;
2015-07-20 20:32:55 +02:00
}
2016-09-15 08:23:39 +02:00
var recordingFileName = _fileSystem . GetValidFilename ( RecordingHelper . GetRecordingName ( timer ) ) . Trim ( ) + ".ts" ;
2015-08-21 04:36:30 +02:00
2016-05-04 22:50:47 +02:00
return Path . Combine ( recordPath , recordingFileName ) ;
}
2015-07-20 20:32:55 +02:00
2017-10-23 21:14:11 +02:00
private async Task RecordStream ( TimerInfo timer , DateTime recordingEndDate , ActiveRecordingInfo activeRecordingInfo )
2016-05-04 22:50:47 +02:00
{
if ( timer = = null )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( timer ) ) ;
2016-05-04 22:50:47 +02:00
}
2015-07-20 20:32:55 +02:00
2017-09-08 18:13:58 +02:00
LiveTvProgram programInfo = null ;
2016-05-04 22:50:47 +02:00
2016-09-15 08:23:39 +02:00
if ( ! string . IsNullOrWhiteSpace ( timer . ProgramId ) )
2016-05-04 22:50:47 +02:00
{
2017-09-08 18:13:58 +02:00
programInfo = GetProgramInfoFromCache ( timer ) ;
2016-05-04 22:50:47 +02:00
}
2019-12-04 21:39:27 +01:00
2016-09-15 08:23:39 +02:00
if ( programInfo = = null )
2016-05-04 22:50:47 +02:00
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Unable to find program with Id {0}. Will search using start date" , timer . ProgramId ) ;
2016-09-15 08:23:39 +02:00
programInfo = GetProgramInfoFromCache ( timer . ChannelId , timer . StartDate ) ;
2015-07-20 20:32:55 +02:00
}
2016-09-15 08:23:39 +02:00
if ( programInfo ! = null )
2016-05-04 22:50:47 +02:00
{
2017-09-08 18:13:58 +02:00
CopyProgramInfoToTimerInfo ( programInfo , timer ) ;
2016-05-04 22:50:47 +02:00
}
2018-09-12 19:26:21 +02:00
var remoteMetadata = await FetchInternetMetadata ( timer , CancellationToken . None ) . ConfigureAwait ( false ) ;
2019-12-04 21:39:27 +01:00
var recordPath = GetRecordingPath ( timer , remoteMetadata , out string seriesPath ) ;
2016-05-04 22:50:47 +02:00
var recordingStatus = RecordingStatus . New ;
2016-09-25 20:39:13 +02:00
2016-09-29 14:55:49 +02:00
string liveStreamId = null ;
2016-05-04 22:50:47 +02:00
2018-09-12 19:26:21 +02:00
var channelItem = _liveTvManager . GetLiveTvChannel ( timer , this ) ;
2015-07-20 20:32:55 +02:00
try
{
2019-12-04 21:39:27 +01:00
var allMediaSources = await _mediaSourceManager . GetPlaybackMediaSources ( channelItem , null , true , false , CancellationToken . None ) . ConfigureAwait ( false ) ;
2018-09-12 19:26:21 +02:00
var mediaStreamInfo = allMediaSources [ 0 ] ;
IDirectStreamProvider directStreamProvider = null ;
2016-09-25 20:39:13 +02:00
2018-09-12 19:26:21 +02:00
if ( mediaStreamInfo . RequiresOpening )
{
2019-12-04 21:39:27 +01:00
var liveStreamResponse = await _mediaSourceManager . OpenLiveStreamInternal (
new LiveStreamRequest
{
ItemId = channelItem . Id ,
OpenToken = mediaStreamInfo . OpenToken
} ,
CancellationToken . None ) . ConfigureAwait ( false ) ;
2018-09-12 19:26:21 +02:00
mediaStreamInfo = liveStreamResponse . Item1 . MediaSource ;
liveStreamId = mediaStreamInfo . LiveStreamId ;
directStreamProvider = liveStreamResponse . Item2 ;
}
var recorder = GetRecorder ( mediaStreamInfo ) ;
2015-09-26 04:31:13 +02:00
2016-06-28 00:53:42 +02:00
recordPath = recorder . GetOutputPath ( mediaStreamInfo , recordPath ) ;
recordPath = EnsureFileUnique ( recordPath , timer . Id ) ;
2016-02-22 17:00:17 +01:00
2016-06-28 00:53:42 +02:00
_libraryMonitor . ReportFileSystemChangeBeginning ( recordPath ) ;
2016-04-26 04:16:46 +02:00
2016-06-28 00:53:42 +02:00
var duration = recordingEndDate - DateTime . UtcNow ;
2015-10-16 20:11:11 +02:00
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Beginning recording. Will record for {0} minutes." , duration . TotalMinutes . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-02-06 22:32:02 +01:00
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Writing file to path: " + recordPath ) ;
2016-02-06 22:32:02 +01:00
2017-09-18 18:52:22 +02:00
Action onStarted = async ( ) = >
2016-06-28 00:53:42 +02:00
{
2017-10-23 21:14:11 +02:00
activeRecordingInfo . Path = recordPath ;
_activeRecordings . TryAdd ( timer . Id , activeRecordingInfo ) ;
2016-06-28 00:53:42 +02:00
timer . Status = RecordingStatus . InProgress ;
_timerProvider . AddOrUpdate ( timer , false ) ;
2016-04-26 04:16:46 +02:00
2017-09-18 18:52:22 +02:00
await SaveRecordingMetadata ( timer , recordPath , seriesPath ) . ConfigureAwait ( false ) ;
2017-10-20 18:16:56 +02:00
2018-09-12 19:26:21 +02:00
await CreateRecordingFolders ( ) . ConfigureAwait ( false ) ;
2017-10-20 18:16:56 +02:00
2017-08-23 21:45:52 +02:00
TriggerRefresh ( recordPath ) ;
2020-05-25 23:52:51 +02:00
await EnforceKeepUpTo ( timer , seriesPath ) . ConfigureAwait ( false ) ;
2016-06-28 00:53:42 +02:00
} ;
2016-04-26 04:16:46 +02:00
2018-09-12 19:26:21 +02:00
await recorder . Record ( directStreamProvider , mediaStreamInfo , recordPath , duration , onStarted , activeRecordingInfo . CancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
2016-06-28 00:53:42 +02:00
recordingStatus = RecordingStatus . Completed ;
2018-12-20 13:11:26 +01:00
_logger . LogInformation ( "Recording completed: {recordPath}" , recordPath ) ;
2015-07-20 20:32:55 +02:00
}
catch ( OperationCanceledException )
{
2018-12-20 13:11:26 +01:00
_logger . LogInformation ( "Recording stopped: {recordPath}" , recordPath ) ;
2016-05-04 22:50:47 +02:00
recordingStatus = RecordingStatus . Completed ;
2015-07-20 20:32:55 +02:00
}
2015-08-05 05:43:54 +02:00
catch ( Exception ex )
2015-07-20 20:32:55 +02:00
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error recording to {recordPath}" , recordPath ) ;
2016-05-04 22:50:47 +02:00
recordingStatus = RecordingStatus . Error ;
2015-07-20 20:32:55 +02:00
}
2016-09-25 20:39:13 +02:00
2016-09-29 14:55:49 +02:00
if ( ! string . IsNullOrWhiteSpace ( liveStreamId ) )
2015-09-04 03:34:57 +02:00
{
2016-09-25 20:39:13 +02:00
try
{
2018-09-12 19:26:21 +02:00
await _mediaSourceManager . CloseLiveStream ( liveStreamId ) . ConfigureAwait ( false ) ;
2016-09-25 20:39:13 +02:00
}
catch ( Exception ex )
2016-06-28 00:53:42 +02:00
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error closing live stream" ) ;
2016-06-28 00:53:42 +02:00
}
2016-09-25 20:39:13 +02:00
}
2016-06-28 00:53:42 +02:00
2017-11-26 05:48:12 +01:00
DeleteFileIfEmpty ( recordPath ) ;
2017-08-23 21:45:52 +02:00
TriggerRefresh ( recordPath ) ;
2017-09-18 18:52:22 +02:00
_libraryMonitor . ReportFileSystemChangeComplete ( recordPath , false ) ;
2016-06-28 00:53:42 +02:00
2019-01-13 21:46:33 +01:00
_activeRecordings . TryRemove ( timer . Id , out var removed ) ;
2015-07-20 20:32:55 +02:00
2017-01-26 07:26:58 +01:00
if ( recordingStatus ! = RecordingStatus . Completed & & DateTime . UtcNow < timer . EndDate & & timer . RetryCount < 10 )
2015-09-03 03:40:47 +02:00
{
2019-12-04 21:39:27 +01:00
const int RetryIntervalSeconds = 60 ;
_logger . LogInformation ( "Retrying recording in {0} seconds." , RetryIntervalSeconds ) ;
2015-09-03 03:40:47 +02:00
2016-06-21 00:07:18 +02:00
timer . Status = RecordingStatus . New ;
2017-10-24 07:19:06 +02:00
timer . PrePaddingSeconds = 0 ;
2019-12-04 21:39:27 +01:00
timer . StartDate = DateTime . UtcNow . AddSeconds ( RetryIntervalSeconds ) ;
2017-01-26 07:26:58 +01:00
timer . RetryCount + + ;
2016-06-21 00:07:18 +02:00
_timerProvider . AddOrUpdate ( timer ) ;
2015-09-04 03:34:57 +02:00
}
2019-01-26 22:59:53 +01:00
else if ( File . Exists ( recordPath ) )
2016-09-26 20:59:18 +02:00
{
2016-09-27 07:13:56 +02:00
timer . RecordingPath = recordPath ;
2016-09-26 20:59:18 +02:00
timer . Status = RecordingStatus . Completed ;
_timerProvider . AddOrUpdate ( timer , false ) ;
OnSuccessfulRecording ( timer , recordPath ) ;
}
2015-09-04 03:34:57 +02:00
else
{
_timerProvider . Delete ( timer ) ;
2015-08-22 21:46:55 +02:00
}
2016-10-11 08:46:59 +02:00
}
2018-09-12 19:26:21 +02:00
private async Task < RemoteSearchResult > FetchInternetMetadata ( TimerInfo timer , CancellationToken cancellationToken )
{
if ( timer . IsSeries )
{
if ( timer . SeriesProviderIds . Count = = 0 )
{
return null ;
}
var query = new RemoteSearchQuery < SeriesInfo > ( )
{
SearchInfo = new SeriesInfo
{
ProviderIds = timer . SeriesProviderIds ,
Name = timer . Name ,
MetadataCountryCode = _config . Configuration . MetadataCountryCode ,
MetadataLanguage = _config . Configuration . PreferredMetadataLanguage
}
} ;
var results = await _providerManager . GetRemoteSearchResults < Series , SeriesInfo > ( query , cancellationToken ) . ConfigureAwait ( false ) ;
return results . FirstOrDefault ( ) ;
}
return null ;
}
2017-11-26 05:48:12 +01:00
private void DeleteFileIfEmpty ( string path )
{
var file = _fileSystem . GetFileInfo ( path ) ;
if ( file . Exists & & file . Length = = 0 )
{
try
{
_fileSystem . DeleteFile ( path ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error deleting 0-byte failed recording file {path}" , path ) ;
2017-11-26 05:48:12 +01:00
}
}
}
2017-08-23 21:45:52 +02:00
private void TriggerRefresh ( string path )
{
2018-12-20 13:11:26 +01:00
_logger . LogInformation ( "Triggering refresh on {path}" , path ) ;
2017-09-18 18:52:22 +02:00
2019-01-26 21:47:11 +01:00
var item = GetAffectedBaseItem ( Path . GetDirectoryName ( path ) ) ;
2017-08-23 21:45:52 +02:00
if ( item ! = null )
{
2018-12-20 13:11:26 +01:00
_logger . LogInformation ( "Refreshing recording parent {path}" , item . Path ) ;
2017-09-18 18:52:22 +02:00
2019-09-10 22:37:53 +02:00
_providerManager . QueueRefresh (
item . Id ,
new MetadataRefreshOptions ( new DirectoryService ( _fileSystem ) )
2017-09-18 18:52:22 +02:00
{
2019-09-10 22:37:53 +02:00
RefreshPaths = new string [ ]
{
path ,
Path . GetDirectoryName ( path ) ,
Path . GetDirectoryName ( Path . GetDirectoryName ( path ) )
}
} ,
RefreshPriority . High ) ;
2017-08-23 21:45:52 +02:00
}
}
private BaseItem GetAffectedBaseItem ( string path )
{
BaseItem item = null ;
2019-01-26 21:47:11 +01:00
var parentPath = Path . GetDirectoryName ( path ) ;
2017-09-18 18:52:22 +02:00
2017-08-23 21:45:52 +02:00
while ( item = = null & & ! string . IsNullOrEmpty ( path ) )
{
item = _libraryManager . FindByPath ( path , null ) ;
2019-01-26 21:47:11 +01:00
path = Path . GetDirectoryName ( path ) ;
2017-08-23 21:45:52 +02:00
}
if ( item ! = null )
{
2017-09-18 18:52:22 +02:00
if ( item . GetType ( ) = = typeof ( Folder ) & & string . Equals ( item . Path , parentPath , StringComparison . OrdinalIgnoreCase ) )
2017-08-23 21:45:52 +02:00
{
2017-09-18 18:52:22 +02:00
var parentItem = item . GetParent ( ) ;
if ( parentItem ! = null & & ! ( parentItem is AggregateFolder ) )
2017-08-23 21:45:52 +02:00
{
2017-09-18 18:52:22 +02:00
item = parentItem ;
2017-08-23 21:45:52 +02:00
}
}
}
return item ;
}
2020-05-25 23:52:51 +02:00
private async Task EnforceKeepUpTo ( TimerInfo timer , string seriesPath )
2016-09-27 07:13:56 +02:00
{
if ( string . IsNullOrWhiteSpace ( timer . SeriesTimerId ) )
{
return ;
}
2019-12-04 21:39:27 +01:00
2017-04-15 21:46:07 +02:00
if ( string . IsNullOrWhiteSpace ( seriesPath ) )
{
return ;
}
2016-09-27 07:13:56 +02:00
var seriesTimerId = timer . SeriesTimerId ;
var seriesTimer = _seriesTimerProvider . GetAll ( ) . FirstOrDefault ( i = > string . Equals ( i . Id , seriesTimerId , StringComparison . OrdinalIgnoreCase ) ) ;
2018-09-12 19:26:21 +02:00
if ( seriesTimer = = null | | seriesTimer . KeepUpTo < = 0 )
2016-09-27 07:13:56 +02:00
{
return ;
}
if ( _disposed )
{
return ;
}
await _recordingDeleteSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
try
{
if ( _disposed )
{
return ;
}
var timersToDelete = _timerProvider . GetAll ( )
. Where ( i = > i . Status = = RecordingStatus . Completed & & ! string . IsNullOrWhiteSpace ( i . RecordingPath ) )
. Where ( i = > string . Equals ( i . SeriesTimerId , seriesTimerId , StringComparison . OrdinalIgnoreCase ) )
. OrderByDescending ( i = > i . EndDate )
2019-01-26 22:59:53 +01:00
. Where ( i = > File . Exists ( i . RecordingPath ) )
2016-09-27 07:13:56 +02:00
. Skip ( seriesTimer . KeepUpTo - 1 )
. ToList ( ) ;
2018-09-12 19:26:21 +02:00
DeleteLibraryItemsForTimers ( timersToDelete ) ;
2017-04-15 21:46:07 +02:00
var librarySeries = _libraryManager . FindByPath ( seriesPath , true ) as Folder ;
if ( librarySeries = = null )
{
return ;
}
2019-12-04 21:39:27 +01:00
var episodesToDelete = librarySeries . GetItemList (
new InternalItemsQuery
{
2020-01-10 21:16:46 +01:00
OrderBy = new [ ] { ( ItemSortBy . DateCreated , SortOrder . Descending ) } ,
2019-12-04 21:39:27 +01:00
IsVirtualItem = false ,
IsFolder = false ,
Recursive = true ,
DtoOptions = new DtoOptions ( true )
2020-01-10 21:16:46 +01:00
} )
2019-01-26 22:59:53 +01:00
. Where ( i = > i . IsFileProtocol & & File . Exists ( i . Path ) )
2017-04-15 21:46:07 +02:00
. Skip ( seriesTimer . KeepUpTo - 1 )
. ToList ( ) ;
foreach ( var item in episodesToDelete )
{
try
{
2019-12-04 21:39:27 +01:00
_libraryManager . DeleteItem (
item ,
new DeleteOptions
{
DeleteFileLocation = true
} ,
true ) ;
2017-04-15 21:46:07 +02:00
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error deleting item" ) ;
2017-04-15 21:46:07 +02:00
}
}
2016-09-27 07:13:56 +02:00
}
finally
{
_recordingDeleteSemaphore . Release ( ) ;
}
}
2018-09-12 19:26:21 +02:00
private void DeleteLibraryItemsForTimers ( List < TimerInfo > timers )
2016-09-27 07:13:56 +02:00
{
foreach ( var timer in timers )
{
if ( _disposed )
{
return ;
}
try
{
2018-09-12 19:26:21 +02:00
DeleteLibraryItemForTimer ( timer ) ;
2016-09-27 07:13:56 +02:00
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error deleting recording" ) ;
2016-09-27 07:13:56 +02:00
}
}
}
2018-09-12 19:26:21 +02:00
private void DeleteLibraryItemForTimer ( TimerInfo timer )
2016-09-27 07:13:56 +02:00
{
var libraryItem = _libraryManager . FindByPath ( timer . RecordingPath , false ) ;
if ( libraryItem ! = null )
{
2019-12-04 21:39:27 +01:00
_libraryManager . DeleteItem (
libraryItem ,
new DeleteOptions
{
DeleteFileLocation = true
} ,
true ) ;
2016-09-27 07:13:56 +02:00
}
2020-01-10 21:16:46 +01:00
else if ( File . Exists ( timer . RecordingPath ) )
2016-09-27 07:13:56 +02:00
{
2020-01-10 21:16:46 +01:00
_fileSystem . DeleteFile ( timer . RecordingPath ) ;
2016-09-27 07:13:56 +02:00
}
_timerProvider . Delete ( timer ) ;
}
2016-03-01 05:24:42 +01:00
private string EnsureFileUnique ( string path , string timerId )
2016-02-26 05:09:42 +01:00
{
var originalPath = path ;
var index = 1 ;
2016-03-01 05:24:42 +01:00
while ( FileExists ( path , timerId ) )
2016-02-26 05:09:42 +01:00
{
2019-01-26 21:47:11 +01:00
var parent = Path . GetDirectoryName ( originalPath ) ;
2016-02-26 05:09:42 +01:00
var name = Path . GetFileNameWithoutExtension ( originalPath ) ;
2017-09-29 08:12:52 +02:00
name + = " - " + index . ToString ( CultureInfo . InvariantCulture ) ;
2016-02-26 05:09:42 +01:00
path = Path . ChangeExtension ( Path . Combine ( parent , name ) , Path . GetExtension ( originalPath ) ) ;
index + + ;
}
return path ;
}
2016-03-01 05:24:42 +01:00
private bool FileExists ( string path , string timerId )
{
2019-01-26 22:59:53 +01:00
if ( File . Exists ( path ) )
2016-03-01 05:24:42 +01:00
{
return true ;
}
2019-12-04 21:39:27 +01:00
return _activeRecordings
2021-04-30 15:09:36 +02:00
. Any ( i = > string . Equals ( i . Value . Path , path , StringComparison . OrdinalIgnoreCase ) & & ! string . Equals ( i . Value . Timer . Id , timerId , StringComparison . OrdinalIgnoreCase ) ) ;
2016-03-01 05:24:42 +01:00
}
2018-09-12 19:26:21 +02:00
private IRecorder GetRecorder ( MediaSourceInfo mediaSource )
2016-02-12 08:01:38 +01:00
{
2018-09-12 19:26:21 +02:00
if ( mediaSource . RequiresLooping | | ! ( mediaSource . Container ? ? string . Empty ) . EndsWith ( "ts" , StringComparison . OrdinalIgnoreCase ) | | ( mediaSource . Protocol ! = MediaProtocol . File & & mediaSource . Protocol ! = MediaProtocol . Http ) )
2017-05-15 21:47:16 +02:00
{
2020-12-23 13:12:40 +01:00
return new EncodedRecorder ( _logger , _mediaEncoder , _config . ApplicationPaths , _config ) ;
2016-02-12 08:01:38 +01:00
}
2020-08-31 19:39:40 +02:00
return new DirectRecorder ( _logger , _httpClientFactory , _streamHelper ) ;
2016-02-12 08:01:38 +01:00
}
2017-05-26 08:48:54 +02:00
private void OnSuccessfulRecording ( TimerInfo timer , string path )
2015-08-22 21:46:55 +02:00
{
2016-11-22 20:45:55 +01:00
PostProcessRecording ( timer , path ) ;
}
private void PostProcessRecording ( TimerInfo timer , string path )
{
var options = GetConfiguration ( ) ;
if ( string . IsNullOrWhiteSpace ( options . RecordingPostProcessor ) )
{
return ;
}
try
{
2020-04-11 19:25:50 +02:00
var process = new Process
{
StartInfo = new ProcessStartInfo
{
Arguments = GetPostProcessArguments ( path , options . RecordingPostProcessorArguments ) ,
CreateNoWindow = true ,
ErrorDialog = false ,
FileName = options . RecordingPostProcessor ,
WindowStyle = ProcessWindowStyle . Hidden ,
UseShellExecute = false
} ,
EnableRaisingEvents = true
2020-03-27 00:45:48 +01:00
} ;
2016-11-22 20:45:55 +01:00
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Running recording post processor {0} {1}" , process . StartInfo . FileName , process . StartInfo . Arguments ) ;
2016-11-22 20:45:55 +01:00
process . Exited + = Process_Exited ;
process . Start ( ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error running recording post processor" ) ;
2016-11-22 20:45:55 +01:00
}
}
2019-01-06 21:50:43 +01:00
private static string GetPostProcessArguments ( string path , string arguments )
2016-11-22 20:45:55 +01:00
{
return arguments . Replace ( "{path}" , path , StringComparison . OrdinalIgnoreCase ) ;
}
private void Process_Exited ( object sender , EventArgs e )
{
2020-04-05 15:23:44 +02:00
using ( var process = ( Process ) sender )
2020-03-27 00:10:16 +01:00
{
2020-04-05 15:23:44 +02:00
_logger . LogInformation ( "Recording post-processing script completed with exit code {ExitCode}" , process . ExitCode ) ;
2016-12-08 06:58:38 +01:00
}
2016-09-16 01:19:27 +02:00
}
2016-11-30 20:50:39 +01:00
private async Task SaveRecordingImage ( string recordingPath , LiveTvProgram program , ItemImageInfo image )
2016-09-16 01:19:27 +02:00
{
2016-11-30 20:50:39 +01:00
if ( ! image . IsLocalFile )
2016-09-20 21:38:53 +02:00
{
2016-11-30 20:50:39 +01:00
image = await _libraryManager . ConvertImageToLocal ( program , image , 0 ) . ConfigureAwait ( false ) ;
}
2016-11-27 01:40:15 +01:00
2019-12-04 21:39:27 +01:00
string imageSaveFilenameWithoutExtension = image . Type switch
2016-11-30 20:50:39 +01:00
{
2019-12-04 21:39:27 +01:00
ImageType . Primary = > program . IsSeries ? Path . GetFileNameWithoutExtension ( recordingPath ) + "-thumb" : "poster" ,
ImageType . Logo = > "logo" ,
ImageType . Thumb = > program . IsSeries ? Path . GetFileNameWithoutExtension ( recordingPath ) + "-thumb" : "landscape" ,
ImageType . Backdrop = > "fanart" ,
_ = > null
} ;
2016-11-30 20:50:39 +01:00
2019-12-04 21:39:27 +01:00
if ( imageSaveFilenameWithoutExtension = = null )
2016-11-30 20:50:39 +01:00
{
return ;
}
2019-01-26 21:47:11 +01:00
var imageSavePath = Path . Combine ( Path . GetDirectoryName ( recordingPath ) , imageSaveFilenameWithoutExtension ) ;
2016-11-30 20:50:39 +01:00
// preserve original image extension
imageSavePath = Path . ChangeExtension ( imageSavePath , Path . GetExtension ( image . Path ) ) ;
2019-01-26 22:31:59 +01:00
File . Copy ( image . Path , imageSavePath , true ) ;
2016-11-30 20:50:39 +01:00
}
private async Task SaveRecordingImages ( string recordingPath , LiveTvProgram program )
{
2017-11-20 22:27:49 +01:00
var image = program . IsSeries ?
( program . GetImageInfo ( ImageType . Thumb , 0 ) ? ? program . GetImageInfo ( ImageType . Primary , 0 ) ) :
( program . GetImageInfo ( ImageType . Primary , 0 ) ? ? program . GetImageInfo ( ImageType . Thumb , 0 ) ) ;
2016-11-30 20:50:39 +01:00
2017-11-20 22:27:49 +01:00
if ( image ! = null )
2016-11-30 20:50:39 +01:00
{
try
2016-11-13 22:04:21 +01:00
{
2016-11-30 20:50:39 +01:00
await SaveRecordingImage ( recordingPath , program , image ) . ConfigureAwait ( false ) ;
2016-11-13 22:04:21 +01:00
}
2016-11-30 20:50:39 +01:00
catch ( Exception ex )
2016-11-13 22:04:21 +01:00
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error saving recording image" ) ;
2016-11-13 22:04:21 +01:00
}
2016-11-30 20:50:39 +01:00
}
if ( ! program . IsSeries )
{
image = program . GetImageInfo ( ImageType . Backdrop , 0 ) ;
if ( image ! = null )
{
try
{
await SaveRecordingImage ( recordingPath , program , image ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error saving recording image" ) ;
2016-11-30 20:50:39 +01:00
}
}
image = program . GetImageInfo ( ImageType . Thumb , 0 ) ;
if ( image ! = null )
2016-11-13 22:04:21 +01:00
{
2016-11-30 20:50:39 +01:00
try
{
await SaveRecordingImage ( recordingPath , program , image ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error saving recording image" ) ;
2016-11-30 20:50:39 +01:00
}
2016-11-13 22:04:21 +01:00
}
2016-11-30 20:50:39 +01:00
image = program . GetImageInfo ( ImageType . Logo , 0 ) ;
if ( image ! = null )
{
try
{
await SaveRecordingImage ( recordingPath , program , image ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error saving recording image" ) ;
2016-11-30 20:50:39 +01:00
}
}
}
}
2017-09-18 18:52:22 +02:00
private async Task SaveRecordingMetadata ( TimerInfo timer , string recordingPath , string seriesPath )
2016-11-30 20:50:39 +01:00
{
try
{
var program = string . IsNullOrWhiteSpace ( timer . ProgramId ) ? null : _libraryManager . GetItemList ( new InternalItemsQuery
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new [ ] { nameof ( LiveTvProgram ) } ,
2016-11-30 20:50:39 +01:00
Limit = 1 ,
2017-05-21 09:25:49 +02:00
ExternalId = timer . ProgramId ,
DtoOptions = new DtoOptions ( true )
2016-11-30 20:50:39 +01:00
} ) . FirstOrDefault ( ) as LiveTvProgram ;
2016-11-27 01:40:15 +01:00
// dummy this up
if ( program = = null )
{
program = new LiveTvProgram
{
Name = timer . Name ,
Overview = timer . Overview ,
Genres = timer . Genres ,
CommunityRating = timer . CommunityRating ,
OfficialRating = timer . OfficialRating ,
ProductionYear = timer . ProductionYear ,
PremiereDate = timer . OriginalAirDate ,
IndexNumber = timer . EpisodeNumber ,
ParentIndexNumber = timer . SeasonNumber
} ;
}
2016-11-30 20:50:39 +01:00
if ( timer . IsSports )
{
2018-09-12 19:26:21 +02:00
program . AddGenre ( "Sports" ) ;
2016-11-30 20:50:39 +01:00
}
2019-12-04 21:39:27 +01:00
2016-11-30 20:50:39 +01:00
if ( timer . IsKids )
{
2018-09-12 19:26:21 +02:00
program . AddGenre ( "Kids" ) ;
program . AddGenre ( "Children" ) ;
2016-11-30 20:50:39 +01:00
}
2019-12-04 21:39:27 +01:00
2016-11-30 20:50:39 +01:00
if ( timer . IsNews )
{
2018-09-12 19:26:21 +02:00
program . AddGenre ( "News" ) ;
2016-11-30 20:50:39 +01:00
}
2016-09-20 21:38:53 +02:00
if ( timer . IsProgramSeries )
{
2016-11-27 01:40:15 +01:00
SaveSeriesNfo ( timer , seriesPath ) ;
SaveVideoNfo ( timer , recordingPath , program , false ) ;
2016-09-20 21:38:53 +02:00
}
2016-10-03 08:28:45 +02:00
else if ( ! timer . IsMovie | | timer . IsSports | | timer . IsNews )
2016-09-20 21:38:53 +02:00
{
2016-11-27 01:40:15 +01:00
SaveVideoNfo ( timer , recordingPath , program , true ) ;
}
else
{
SaveVideoNfo ( timer , recordingPath , program , false ) ;
2016-09-20 21:38:53 +02:00
}
2016-11-30 20:50:39 +01:00
await SaveRecordingImages ( recordingPath , program ) . ConfigureAwait ( false ) ;
2016-09-20 21:38:53 +02:00
}
catch ( Exception ex )
2016-09-16 01:19:27 +02:00
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error saving nfo" ) ;
2016-09-16 01:19:27 +02:00
}
}
2016-11-27 01:40:15 +01:00
private void SaveSeriesNfo ( TimerInfo timer , string seriesPath )
2016-09-16 01:19:27 +02:00
{
var nfoPath = Path . Combine ( seriesPath , "tvshow.nfo" ) ;
2019-01-26 22:59:53 +01:00
if ( File . Exists ( nfoPath ) )
2016-09-16 01:19:27 +02:00
{
return ;
}
2021-03-07 14:43:28 +01:00
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using ( var stream = new FileStream ( nfoPath , FileMode . Create , FileAccess . Write , FileShare . None ) )
2016-09-16 01:19:27 +02:00
{
var settings = new XmlWriterSettings
{
Indent = true ,
Encoding = Encoding . UTF8 ,
CloseOutput = false
} ;
2019-01-13 21:37:13 +01:00
using ( var writer = XmlWriter . Create ( stream , settings ) )
2016-09-16 01:19:27 +02:00
{
writer . WriteStartDocument ( true ) ;
writer . WriteStartElement ( "tvshow" ) ;
2019-01-17 18:47:41 +01:00
string id ;
2020-06-06 21:17:49 +02:00
if ( timer . SeriesProviderIds . TryGetValue ( MetadataProvider . Tvdb . ToString ( ) , out id ) )
2018-09-12 19:26:21 +02:00
{
writer . WriteElementString ( "id" , id ) ;
}
2019-12-04 21:39:27 +01:00
2020-06-06 21:17:49 +02:00
if ( timer . SeriesProviderIds . TryGetValue ( MetadataProvider . Imdb . ToString ( ) , out id ) )
2018-09-12 19:26:21 +02:00
{
writer . WriteElementString ( "imdb_id" , id ) ;
}
2019-12-04 21:39:27 +01:00
2020-06-06 21:17:49 +02:00
if ( timer . SeriesProviderIds . TryGetValue ( MetadataProvider . Tmdb . ToString ( ) , out id ) )
2018-09-12 19:26:21 +02:00
{
writer . WriteElementString ( "tmdbid" , id ) ;
}
2019-12-04 21:39:27 +01:00
2020-06-06 21:17:49 +02:00
if ( timer . SeriesProviderIds . TryGetValue ( MetadataProvider . Zap2It . ToString ( ) , out id ) )
2018-09-12 19:26:21 +02:00
{
writer . WriteElementString ( "zap2itid" , id ) ;
}
2016-09-16 01:19:27 +02:00
if ( ! string . IsNullOrWhiteSpace ( timer . Name ) )
2015-08-22 21:46:55 +02:00
{
2016-09-16 01:19:27 +02:00
writer . WriteElementString ( "title" , timer . Name ) ;
2015-08-22 21:46:55 +02:00
}
2016-09-16 01:19:27 +02:00
2018-09-12 19:26:21 +02:00
if ( ! string . IsNullOrWhiteSpace ( timer . OfficialRating ) )
2016-11-13 22:04:21 +01:00
{
writer . WriteElementString ( "mpaa" , timer . OfficialRating ) ;
}
foreach ( var genre in timer . Genres )
{
writer . WriteElementString ( "genre" , genre ) ;
}
2016-09-16 01:19:27 +02:00
writer . WriteEndElement ( ) ;
writer . WriteEndDocument ( ) ;
2015-08-22 21:46:55 +02:00
}
}
2015-07-20 20:32:55 +02:00
}
2016-11-27 01:40:15 +01:00
private void SaveVideoNfo ( TimerInfo timer , string recordingPath , BaseItem item , bool lockData )
2016-09-20 21:38:53 +02:00
{
var nfoPath = Path . ChangeExtension ( recordingPath , ".nfo" ) ;
2019-01-26 22:59:53 +01:00
if ( File . Exists ( nfoPath ) )
2016-09-20 21:38:53 +02:00
{
return ;
}
2021-03-07 14:43:28 +01:00
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using ( var stream = new FileStream ( nfoPath , FileMode . Create , FileAccess . Write , FileShare . None ) )
2016-09-20 21:38:53 +02:00
{
var settings = new XmlWriterSettings
{
Indent = true ,
Encoding = Encoding . UTF8 ,
CloseOutput = false
} ;
2016-11-27 01:40:15 +01:00
var options = _config . GetNfoConfiguration ( ) ;
2018-09-12 19:26:21 +02:00
var isSeriesEpisode = timer . IsProgramSeries ;
2019-01-13 21:37:13 +01:00
using ( var writer = XmlWriter . Create ( stream , settings ) )
2016-09-20 21:38:53 +02:00
{
writer . WriteStartDocument ( true ) ;
2018-09-12 19:26:21 +02:00
if ( isSeriesEpisode )
2016-09-20 21:38:53 +02:00
{
2016-11-13 22:04:21 +01:00
writer . WriteStartElement ( "episodedetails" ) ;
if ( ! string . IsNullOrWhiteSpace ( timer . EpisodeTitle ) )
{
writer . WriteElementString ( "title" , timer . EpisodeTitle ) ;
}
2018-09-12 19:26:21 +02:00
var premiereDate = item . PremiereDate ? ? ( ! timer . IsRepeat ? DateTime . UtcNow : ( DateTime ? ) null ) ;
if ( premiereDate . HasValue )
2016-11-13 22:04:21 +01:00
{
2016-11-27 01:40:15 +01:00
var formatString = options . ReleaseDateFormat ;
2016-11-13 22:04:21 +01:00
2019-12-04 21:39:27 +01:00
writer . WriteElementString (
"aired" ,
premiereDate . Value . ToLocalTime ( ) . ToString ( formatString , CultureInfo . InvariantCulture ) ) ;
2016-11-13 22:04:21 +01:00
}
2016-11-27 01:40:15 +01:00
if ( item . IndexNumber . HasValue )
2016-11-13 22:04:21 +01:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "episode" , item . IndexNumber . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-11-13 22:04:21 +01:00
}
2016-11-27 01:40:15 +01:00
if ( item . ParentIndexNumber . HasValue )
2016-11-13 22:04:21 +01:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "season" , item . ParentIndexNumber . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-11-13 22:04:21 +01:00
}
}
else
{
writer . WriteStartElement ( "movie" ) ;
2016-11-27 01:40:15 +01:00
if ( ! string . IsNullOrWhiteSpace ( item . Name ) )
2016-11-13 22:04:21 +01:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "title" , item . Name ) ;
}
if ( ! string . IsNullOrWhiteSpace ( item . OriginalTitle ) )
{
writer . WriteElementString ( "originaltitle" , item . OriginalTitle ) ;
}
if ( item . PremiereDate . HasValue )
{
var formatString = options . ReleaseDateFormat ;
2019-12-04 21:39:27 +01:00
writer . WriteElementString (
"premiered" ,
item . PremiereDate . Value . ToLocalTime ( ) . ToString ( formatString , CultureInfo . InvariantCulture ) ) ;
writer . WriteElementString (
"releasedate" ,
item . PremiereDate . Value . ToLocalTime ( ) . ToString ( formatString , CultureInfo . InvariantCulture ) ) ;
2016-11-13 22:04:21 +01:00
}
2016-09-20 21:38:53 +02:00
}
2019-12-04 21:39:27 +01:00
writer . WriteElementString (
"dateadded" ,
DateTime . UtcNow . ToLocalTime ( ) . ToString ( DateAddedFormat , CultureInfo . InvariantCulture ) ) ;
2016-09-20 21:38:53 +02:00
2016-11-27 01:40:15 +01:00
if ( item . ProductionYear . HasValue )
2016-09-20 21:38:53 +02:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "year" , item . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-09-20 21:38:53 +02:00
}
2016-11-27 01:40:15 +01:00
if ( ! string . IsNullOrEmpty ( item . OfficialRating ) )
2016-09-20 21:38:53 +02:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "mpaa" , item . OfficialRating ) ;
}
2016-09-20 21:38:53 +02:00
2016-11-27 01:40:15 +01:00
var overview = ( item . Overview ? ? string . Empty )
2016-09-20 21:38:53 +02:00
. StripHtml ( )
2019-12-04 21:39:27 +01:00
. Replace ( """ , "'" , StringComparison . Ordinal ) ;
2016-09-20 21:38:53 +02:00
writer . WriteElementString ( "plot" , overview ) ;
2016-11-27 01:40:15 +01:00
if ( item . CommunityRating . HasValue )
2016-09-29 14:55:49 +02:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "rating" , item . CommunityRating . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-09-29 14:55:49 +02:00
}
2016-09-21 19:07:18 +02:00
2016-11-27 01:40:15 +01:00
foreach ( var genre in item . Genres )
2016-09-20 21:38:53 +02:00
{
writer . WriteElementString ( "genre" , genre ) ;
}
2018-09-12 19:26:21 +02:00
var people = item . Id . Equals ( Guid . Empty ) ? new List < PersonInfo > ( ) : _libraryManager . GetPeople ( item ) ;
2016-11-27 01:40:15 +01:00
var directors = people
. Where ( i = > IsPersonType ( i , PersonType . Director ) )
. Select ( i = > i . Name )
. ToList ( ) ;
foreach ( var person in directors )
{
writer . WriteElementString ( "director" , person ) ;
}
var writers = people
. Where ( i = > IsPersonType ( i , PersonType . Writer ) )
. Select ( i = > i . Name )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToList ( ) ;
foreach ( var person in writers )
{
writer . WriteElementString ( "writer" , person ) ;
}
foreach ( var person in writers )
{
writer . WriteElementString ( "credits" , person ) ;
}
2020-06-06 21:17:49 +02:00
var tmdbCollection = item . GetProviderId ( MetadataProvider . TmdbCollection ) ;
2016-11-27 01:40:15 +01:00
if ( ! string . IsNullOrEmpty ( tmdbCollection ) )
{
writer . WriteElementString ( "collectionnumber" , tmdbCollection ) ;
}
2020-06-06 21:17:49 +02:00
var imdb = item . GetProviderId ( MetadataProvider . Imdb ) ;
2016-11-27 01:40:15 +01:00
if ( ! string . IsNullOrEmpty ( imdb ) )
{
2018-09-12 19:26:21 +02:00
if ( ! isSeriesEpisode )
2016-11-27 01:40:15 +01:00
{
2018-09-12 19:26:21 +02:00
writer . WriteElementString ( "id" , imdb ) ;
2016-11-27 01:40:15 +01:00
}
2018-09-12 19:26:21 +02:00
writer . WriteElementString ( "imdbid" , imdb ) ;
// No need to lock if we have identified the content already
lockData = false ;
2016-11-27 01:40:15 +01:00
}
2020-06-06 21:17:49 +02:00
var tvdb = item . GetProviderId ( MetadataProvider . Tvdb ) ;
2016-11-27 01:40:15 +01:00
if ( ! string . IsNullOrEmpty ( tvdb ) )
{
writer . WriteElementString ( "tvdbid" , tvdb ) ;
2018-09-12 19:26:21 +02:00
// No need to lock if we have identified the content already
lockData = false ;
2016-11-27 01:40:15 +01:00
}
2020-06-06 21:17:49 +02:00
var tmdb = item . GetProviderId ( MetadataProvider . Tmdb ) ;
2016-11-27 01:40:15 +01:00
if ( ! string . IsNullOrEmpty ( tmdb ) )
{
writer . WriteElementString ( "tmdbid" , tmdb ) ;
2018-09-12 19:26:21 +02:00
// No need to lock if we have identified the content already
lockData = false ;
}
if ( lockData )
{
2019-01-27 12:03:43 +01:00
writer . WriteElementString ( "lockdata" , true . ToString ( CultureInfo . InvariantCulture ) . ToLowerInvariant ( ) ) ;
2016-11-27 01:40:15 +01:00
}
if ( item . CriticRating . HasValue )
{
writer . WriteElementString ( "criticrating" , item . CriticRating . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
}
if ( ! string . IsNullOrWhiteSpace ( item . Tagline ) )
2016-09-20 21:38:53 +02:00
{
2016-11-27 01:40:15 +01:00
writer . WriteElementString ( "tagline" , item . Tagline ) ;
}
foreach ( var studio in item . Studios )
{
writer . WriteElementString ( "studio" , studio ) ;
}
2016-09-20 21:38:53 +02:00
writer . WriteEndElement ( ) ;
writer . WriteEndDocument ( ) ;
}
}
}
2016-11-27 01:40:15 +01:00
private static bool IsPersonType ( PersonInfo person , string type )
2019-12-04 21:39:27 +01:00
= > string . Equals ( person . Type , type , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( person . Role , type , StringComparison . OrdinalIgnoreCase ) ;
2016-09-21 19:07:18 +02:00
2017-09-08 18:13:58 +02:00
private LiveTvProgram GetProgramInfoFromCache ( string programId )
2015-07-20 20:32:55 +02:00
{
2017-09-08 18:13:58 +02:00
var query = new InternalItemsQuery
{
2018-09-12 19:26:21 +02:00
ItemIds = new [ ] { _liveTvManager . GetInternalProgramId ( programId ) } ,
2017-09-08 18:13:58 +02:00
Limit = 1 ,
DtoOptions = new DtoOptions ( )
} ;
return _libraryManager . GetItemList ( query ) . Cast < LiveTvProgram > ( ) . FirstOrDefault ( ) ;
2015-07-20 20:32:55 +02:00
}
2017-09-08 18:13:58 +02:00
private LiveTvProgram GetProgramInfoFromCache ( TimerInfo timer )
2016-02-06 22:32:02 +01:00
{
2017-09-08 18:13:58 +02:00
return GetProgramInfoFromCache ( timer . ProgramId , timer . ChannelId ) ;
}
private LiveTvProgram GetProgramInfoFromCache ( string programId , string channelId )
{
return GetProgramInfoFromCache ( programId ) ;
}
private LiveTvProgram GetProgramInfoFromCache ( string channelId , DateTime startDateUtc )
{
var query = new InternalItemsQuery
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new string [ ] { nameof ( LiveTvProgram ) } ,
2017-09-08 18:13:58 +02:00
Limit = 1 ,
DtoOptions = new DtoOptions ( true )
{
EnableImages = false
} ,
MinStartDate = startDateUtc . AddMinutes ( - 3 ) ,
MaxStartDate = startDateUtc . AddMinutes ( 3 ) ,
2019-10-20 16:08:40 +02:00
OrderBy = new [ ] { ( ItemSortBy . StartDate , SortOrder . Ascending ) }
2017-09-08 18:13:58 +02:00
} ;
if ( ! string . IsNullOrWhiteSpace ( channelId ) )
{
2018-09-12 19:26:21 +02:00
query . ChannelIds = new [ ] { _liveTvManager . GetInternalChannelId ( Name , channelId ) } ;
2017-09-08 18:13:58 +02:00
}
return _libraryManager . GetItemList ( query ) . Cast < LiveTvProgram > ( ) . FirstOrDefault ( ) ;
2016-02-06 22:32:02 +01:00
}
2015-07-20 20:32:55 +02:00
private LiveTvOptions GetConfiguration ( )
{
return _config . GetConfiguration < LiveTvOptions > ( "livetv" ) ;
}
2016-09-26 20:59:18 +02:00
private bool ShouldCancelTimerForSeriesTimer ( SeriesTimerInfo seriesTimer , TimerInfo timer )
{
2017-02-20 08:04:03 +01:00
if ( timer . IsManual )
{
return false ;
}
2019-12-04 21:39:27 +01:00
if ( ! seriesTimer . RecordAnyTime
& & Math . Abs ( seriesTimer . StartDate . TimeOfDay . Ticks - timer . StartDate . TimeOfDay . Ticks ) > = TimeSpan . FromMinutes ( 10 ) . Ticks )
2016-10-04 07:15:39 +02:00
{
2019-12-04 21:39:27 +01:00
return true ;
2016-10-04 07:15:39 +02:00
}
if ( seriesTimer . RecordNewOnly & & timer . IsRepeat )
{
return true ;
}
2019-12-04 21:39:27 +01:00
if ( ! seriesTimer . RecordAnyChannel
& & ! string . Equals ( timer . ChannelId , seriesTimer . ChannelId , StringComparison . OrdinalIgnoreCase ) )
2016-10-04 07:15:39 +02:00
{
return true ;
}
2016-09-26 20:59:18 +02:00
return seriesTimer . SkipEpisodesInLibrary & & IsProgramAlreadyInLibrary ( timer ) ;
}
2016-11-24 17:29:23 +01:00
private void HandleDuplicateShowIds ( List < TimerInfo > timers )
{
foreach ( var timer in timers . Skip ( 1 ) )
{
// TODO: Get smarter, prefer HD, etc
timer . Status = RecordingStatus . Cancelled ;
_timerProvider . Update ( timer ) ;
}
}
private void SearchForDuplicateShowIds ( List < TimerInfo > timers )
{
var groups = timers . ToLookup ( i = > i . ShowId ? ? string . Empty ) . ToList ( ) ;
foreach ( var group in groups )
{
if ( string . IsNullOrWhiteSpace ( group . Key ) )
{
continue ;
}
var groupTimers = group . ToList ( ) ;
if ( groupTimers . Count < 2 )
{
continue ;
}
HandleDuplicateShowIds ( groupTimers ) ;
}
}
2019-02-01 21:56:50 +01:00
private void UpdateTimersForSeriesTimer ( SeriesTimerInfo seriesTimer , bool updateTimerSettings , bool deleteInvalidTimers )
2015-07-20 20:32:55 +02:00
{
2019-02-01 21:56:50 +01:00
var allTimers = GetTimersForSeries ( seriesTimer ) . ToList ( ) ;
2016-09-15 08:23:39 +02:00
2016-11-24 17:29:23 +01:00
var enabledTimersForSeries = new List < TimerInfo > ( ) ;
2019-01-04 22:42:56 +01:00
foreach ( var timer in allTimers )
2015-07-20 20:32:55 +02:00
{
2021-05-05 13:51:14 +02:00
var existingTimer = _timerProvider . GetTimer ( timer . Id )
? ? ( string . IsNullOrWhiteSpace ( timer . ProgramId )
2019-01-04 22:42:56 +01:00
? null
2021-05-05 13:51:14 +02:00
: _timerProvider . GetTimerByProgramId ( timer . ProgramId ) ) ;
2016-09-26 20:59:18 +02:00
2019-01-04 22:42:56 +01:00
if ( existingTimer = = null )
{
if ( ShouldCancelTimerForSeriesTimer ( seriesTimer , timer ) )
2017-03-26 06:21:32 +02:00
{
2019-01-04 22:42:56 +01:00
timer . Status = RecordingStatus . Cancelled ;
2017-03-26 06:21:32 +02:00
}
2019-01-04 22:42:56 +01:00
else
2017-03-26 06:21:32 +02:00
{
2019-01-04 22:42:56 +01:00
enabledTimersForSeries . Add ( timer ) ;
2017-03-26 06:21:32 +02:00
}
2019-12-04 21:39:27 +01:00
2019-01-04 22:42:56 +01:00
_timerProvider . Add ( timer ) ;
2017-03-26 06:21:32 +02:00
2019-01-07 20:10:26 +01:00
TimerCreated ? . Invoke ( this , new GenericEventArgs < TimerInfo > ( timer ) ) ;
2019-01-04 22:42:56 +01:00
}
2019-12-04 21:39:27 +01:00
2019-01-07 20:35:26 +01:00
// Only update if not currently active - test both new timer and existing in case Id's are different
// Id's could be different if the timer was created manually prior to series timer creation
else if ( ! _activeRecordings . TryGetValue ( timer . Id , out _ ) & & ! _activeRecordings . TryGetValue ( existingTimer . Id , out _ ) )
2019-01-04 22:42:56 +01:00
{
2019-01-07 20:35:26 +01:00
UpdateExistingTimerWithNewMetadata ( existingTimer , timer ) ;
2019-01-04 22:42:56 +01:00
2019-01-07 20:35:26 +01:00
// Needed by ShouldCancelTimerForSeriesTimer
timer . IsManual = existingTimer . IsManual ;
2017-03-26 06:21:32 +02:00
2019-01-07 20:35:26 +01:00
if ( ShouldCancelTimerForSeriesTimer ( seriesTimer , timer ) )
2016-09-26 20:59:18 +02:00
{
2019-01-07 20:35:26 +01:00
existingTimer . Status = RecordingStatus . Cancelled ;
}
else if ( ! existingTimer . IsManual )
{
existingTimer . Status = RecordingStatus . New ;
}
2018-09-12 19:26:21 +02:00
2019-01-07 20:35:26 +01:00
if ( existingTimer . Status ! = RecordingStatus . Cancelled )
{
enabledTimersForSeries . Add ( existingTimer ) ;
2016-09-26 20:59:18 +02:00
}
2019-01-04 22:42:56 +01:00
2019-01-07 20:35:26 +01:00
if ( updateTimerSettings )
2016-09-26 20:59:18 +02:00
{
2017-03-26 06:21:32 +02:00
// Only update if not currently active - test both new timer and existing in case Id's are different
// Id's could be different if the timer was created manually prior to series timer creation
2019-01-13 21:46:33 +01:00
if ( ! _activeRecordings . TryGetValue ( timer . Id , out var activeRecordingInfo ) & & ! _activeRecordings . TryGetValue ( existingTimer . Id , out activeRecordingInfo ) )
2016-09-26 20:59:18 +02:00
{
2016-10-03 08:28:45 +02:00
UpdateExistingTimerWithNewMetadata ( existingTimer , timer ) ;
2016-09-26 20:59:18 +02:00
2017-03-12 20:27:26 +01:00
// Needed by ShouldCancelTimerForSeriesTimer
timer . IsManual = existingTimer . IsManual ;
2016-09-26 20:59:18 +02:00
if ( ShouldCancelTimerForSeriesTimer ( seriesTimer , timer ) )
{
existingTimer . Status = RecordingStatus . Cancelled ;
}
2017-11-05 22:51:23 +01:00
else if ( ! existingTimer . IsManual )
{
existingTimer . Status = RecordingStatus . New ;
}
2016-09-27 07:13:56 +02:00
2016-11-24 17:29:23 +01:00
if ( existingTimer . Status ! = RecordingStatus . Cancelled )
{
enabledTimersForSeries . Add ( existingTimer ) ;
}
2016-12-20 06:21:21 +01:00
if ( updateTimerSettings )
{
existingTimer . KeepUntil = seriesTimer . KeepUntil ;
existingTimer . IsPostPaddingRequired = seriesTimer . IsPostPaddingRequired ;
existingTimer . IsPrePaddingRequired = seriesTimer . IsPrePaddingRequired ;
existingTimer . PostPaddingSeconds = seriesTimer . PostPaddingSeconds ;
existingTimer . PrePaddingSeconds = seriesTimer . PrePaddingSeconds ;
existingTimer . Priority = seriesTimer . Priority ;
}
2016-11-28 20:27:06 +01:00
2016-09-27 07:13:56 +02:00
existingTimer . SeriesTimerId = seriesTimer . Id ;
2016-09-26 20:59:18 +02:00
_timerProvider . Update ( existingTimer ) ;
}
}
2019-01-07 20:35:26 +01:00
existingTimer . SeriesTimerId = seriesTimer . Id ;
_timerProvider . Update ( existingTimer ) ;
2015-08-21 21:25:35 +02:00
}
2015-07-20 20:32:55 +02:00
}
2016-01-20 04:48:37 +01:00
2016-11-24 17:29:23 +01:00
SearchForDuplicateShowIds ( enabledTimersForSeries ) ;
2016-01-20 04:48:37 +01:00
if ( deleteInvalidTimers )
{
2016-09-26 20:59:18 +02:00
var allTimerIds = allTimers
2016-01-20 04:48:37 +01:00
. Select ( i = > i . Id )
. ToList ( ) ;
2018-09-12 19:26:21 +02:00
var deleteStatuses = new [ ]
2016-09-26 20:59:18 +02:00
{
RecordingStatus . New
} ;
2016-01-20 04:48:37 +01:00
var deletes = _timerProvider . GetAll ( )
. Where ( i = > string . Equals ( i . SeriesTimerId , seriesTimer . Id , StringComparison . OrdinalIgnoreCase ) )
2016-09-26 20:59:18 +02:00
. Where ( i = > ! allTimerIds . Contains ( i . Id , StringComparer . OrdinalIgnoreCase ) & & i . StartDate > DateTime . UtcNow )
. Where ( i = > deleteStatuses . Contains ( i . Status ) )
2016-01-20 04:48:37 +01:00
. ToList ( ) ;
foreach ( var timer in deletes )
{
2018-09-12 19:26:21 +02:00
CancelTimerInternal ( timer . Id , false , false ) ;
2016-01-20 04:48:37 +01:00
}
}
2015-07-20 20:32:55 +02:00
}
2017-09-08 18:13:58 +02:00
private IEnumerable < TimerInfo > GetTimersForSeries ( SeriesTimerInfo seriesTimer )
2015-07-29 19:16:00 +02:00
{
2016-03-13 08:34:17 +01:00
if ( seriesTimer = = null )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( seriesTimer ) ) ;
2016-03-13 08:34:17 +01:00
}
2017-09-08 18:13:58 +02:00
var query = new InternalItemsQuery
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new string [ ] { nameof ( LiveTvProgram ) } ,
2017-09-08 18:13:58 +02:00
ExternalSeriesId = seriesTimer . SeriesId ,
DtoOptions = new DtoOptions ( true )
{
EnableImages = false
} ,
MinEndDate = DateTime . UtcNow
} ;
2018-09-12 19:26:21 +02:00
if ( string . IsNullOrEmpty ( seriesTimer . SeriesId ) )
{
query . Name = seriesTimer . Name ;
}
2017-09-08 18:13:58 +02:00
if ( ! seriesTimer . RecordAnyChannel )
{
2018-09-12 19:26:21 +02:00
query . ChannelIds = new [ ] { _liveTvManager . GetInternalChannelId ( Name , seriesTimer . ChannelId ) } ;
2017-09-08 18:13:58 +02:00
}
2018-09-12 19:26:21 +02:00
var tempChannelCache = new Dictionary < Guid , LiveTvChannel > ( ) ;
2017-09-08 18:13:58 +02:00
return _libraryManager . GetItemList ( query ) . Cast < LiveTvProgram > ( ) . Select ( i = > CreateTimer ( i , seriesTimer , tempChannelCache ) ) ;
}
2018-09-12 19:26:21 +02:00
private TimerInfo CreateTimer ( LiveTvProgram parent , SeriesTimerInfo seriesTimer , Dictionary < Guid , LiveTvChannel > tempChannelCache )
2017-09-08 18:13:58 +02:00
{
string channelId = seriesTimer . RecordAnyChannel ? null : seriesTimer . ChannelId ;
2018-09-12 19:26:21 +02:00
if ( string . IsNullOrWhiteSpace ( channelId ) & & ! parent . ChannelId . Equals ( Guid . Empty ) )
2017-09-08 18:13:58 +02:00
{
2019-01-17 18:47:41 +01:00
if ( ! tempChannelCache . TryGetValue ( parent . ChannelId , out LiveTvChannel channel ) )
2017-09-08 18:13:58 +02:00
{
2019-12-04 21:39:27 +01:00
channel = _libraryManager . GetItemList (
new InternalItemsQuery
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new string [ ] { nameof ( LiveTvChannel ) } ,
2019-12-04 21:39:27 +01:00
ItemIds = new [ ] { parent . ChannelId } ,
DtoOptions = new DtoOptions ( )
} ) . FirstOrDefault ( ) as LiveTvChannel ;
2017-09-08 18:13:58 +02:00
if ( channel ! = null & & ! string . IsNullOrWhiteSpace ( channel . ExternalId ) )
{
tempChannelCache [ parent . ChannelId ] = channel ;
}
}
if ( channel ! = null | | tempChannelCache . TryGetValue ( parent . ChannelId , out channel ) )
{
channelId = channel . ExternalId ;
}
}
var timer = new TimerInfo
{
ChannelId = channelId ,
2019-02-28 23:22:57 +01:00
Id = ( seriesTimer . Id + parent . ExternalId ) . GetMD5 ( ) . ToString ( "N" , CultureInfo . InvariantCulture ) ,
2017-09-08 18:13:58 +02:00
StartDate = parent . StartDate ,
EndDate = parent . EndDate . Value ,
ProgramId = parent . ExternalId ,
PrePaddingSeconds = seriesTimer . PrePaddingSeconds ,
PostPaddingSeconds = seriesTimer . PostPaddingSeconds ,
IsPostPaddingRequired = seriesTimer . IsPostPaddingRequired ,
IsPrePaddingRequired = seriesTimer . IsPrePaddingRequired ,
KeepUntil = seriesTimer . KeepUntil ,
Priority = seriesTimer . Priority ,
Name = parent . Name ,
Overview = parent . Overview ,
SeriesId = parent . ExternalSeriesId ,
SeriesTimerId = seriesTimer . Id ,
ShowId = parent . ShowId
} ;
CopyProgramInfoToTimerInfo ( parent , timer , tempChannelCache ) ;
return timer ;
}
private void CopyProgramInfoToTimerInfo ( LiveTvProgram programInfo , TimerInfo timerInfo )
{
2018-09-12 19:26:21 +02:00
var tempChannelCache = new Dictionary < Guid , LiveTvChannel > ( ) ;
2017-09-08 18:13:58 +02:00
CopyProgramInfoToTimerInfo ( programInfo , timerInfo , tempChannelCache ) ;
}
2018-09-12 19:26:21 +02:00
private void CopyProgramInfoToTimerInfo ( LiveTvProgram programInfo , TimerInfo timerInfo , Dictionary < Guid , LiveTvChannel > tempChannelCache )
2017-09-08 18:13:58 +02:00
{
string channelId = null ;
2018-09-12 19:26:21 +02:00
if ( ! programInfo . ChannelId . Equals ( Guid . Empty ) )
2017-09-08 18:13:58 +02:00
{
2019-01-17 18:47:41 +01:00
if ( ! tempChannelCache . TryGetValue ( programInfo . ChannelId , out LiveTvChannel channel ) )
2017-09-08 18:13:58 +02:00
{
2019-12-04 21:39:27 +01:00
channel = _libraryManager . GetItemList (
new InternalItemsQuery
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new string [ ] { nameof ( LiveTvChannel ) } ,
2019-12-04 21:39:27 +01:00
ItemIds = new [ ] { programInfo . ChannelId } ,
DtoOptions = new DtoOptions ( )
} ) . FirstOrDefault ( ) as LiveTvChannel ;
2017-09-08 18:13:58 +02:00
if ( channel ! = null & & ! string . IsNullOrWhiteSpace ( channel . ExternalId ) )
{
tempChannelCache [ programInfo . ChannelId ] = channel ;
}
}
if ( channel ! = null | | tempChannelCache . TryGetValue ( programInfo . ChannelId , out channel ) )
{
channelId = channel . ExternalId ;
}
}
timerInfo . Name = programInfo . Name ;
timerInfo . StartDate = programInfo . StartDate ;
timerInfo . EndDate = programInfo . EndDate . Value ;
if ( ! string . IsNullOrWhiteSpace ( channelId ) )
2016-03-13 08:34:17 +01:00
{
2017-09-08 18:13:58 +02:00
timerInfo . ChannelId = channelId ;
2016-03-13 08:34:17 +01:00
}
2017-09-08 18:13:58 +02:00
timerInfo . SeasonNumber = programInfo . ParentIndexNumber ;
timerInfo . EpisodeNumber = programInfo . IndexNumber ;
timerInfo . IsMovie = programInfo . IsMovie ;
timerInfo . ProductionYear = programInfo . ProductionYear ;
timerInfo . EpisodeTitle = programInfo . EpisodeTitle ;
timerInfo . OriginalAirDate = programInfo . PremiereDate ;
timerInfo . IsProgramSeries = programInfo . IsSeries ;
2015-08-17 18:52:56 +02:00
2017-09-08 18:13:58 +02:00
timerInfo . IsSeries = programInfo . IsSeries ;
2015-07-29 19:16:00 +02:00
2017-09-08 18:13:58 +02:00
timerInfo . CommunityRating = programInfo . CommunityRating ;
timerInfo . Overview = programInfo . Overview ;
timerInfo . OfficialRating = programInfo . OfficialRating ;
timerInfo . IsRepeat = programInfo . IsRepeat ;
timerInfo . SeriesId = programInfo . ExternalSeriesId ;
2018-09-12 19:26:21 +02:00
timerInfo . ProviderIds = programInfo . ProviderIds ;
timerInfo . Tags = programInfo . Tags ;
var seriesProviderIds = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
foreach ( var providerId in timerInfo . ProviderIds )
{
2019-12-04 21:39:27 +01:00
const string Search = "Series" ;
if ( providerId . Key . StartsWith ( Search , StringComparison . OrdinalIgnoreCase ) )
2018-09-12 19:26:21 +02:00
{
2019-12-04 21:39:27 +01:00
seriesProviderIds [ providerId . Key . Substring ( Search . Length ) ] = providerId . Value ;
2018-09-12 19:26:21 +02:00
}
}
timerInfo . SeriesProviderIds = seriesProviderIds ;
2015-07-29 19:16:00 +02:00
}
2016-09-26 20:59:18 +02:00
private bool IsProgramAlreadyInLibrary ( TimerInfo program )
2016-05-04 22:50:47 +02:00
{
if ( ( program . EpisodeNumber . HasValue & & program . SeasonNumber . HasValue ) | | ! string . IsNullOrWhiteSpace ( program . EpisodeTitle ) )
{
2019-12-04 21:39:27 +01:00
var seriesIds = _libraryManager . GetItemIds (
new InternalItemsQuery
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new [ ] { nameof ( Series ) } ,
2019-12-04 21:39:27 +01:00
Name = program . Name
} ) . ToArray ( ) ;
2016-05-04 22:50:47 +02:00
if ( seriesIds . Length = = 0 )
{
return false ;
}
if ( program . EpisodeNumber . HasValue & & program . SeasonNumber . HasValue )
{
2017-05-21 09:25:49 +02:00
var result = _libraryManager . GetItemIds ( new InternalItemsQuery
2016-05-04 22:50:47 +02:00
{
2020-10-17 16:19:57 +02:00
IncludeItemTypes = new [ ] { nameof ( Episode ) } ,
2016-05-04 22:50:47 +02:00
ParentIndexNumber = program . SeasonNumber . Value ,
IndexNumber = program . EpisodeNumber . Value ,
2016-05-10 07:00:50 +02:00
AncestorIds = seriesIds ,
2017-05-21 09:25:49 +02:00
IsVirtualItem = false ,
Limit = 1
2016-05-04 22:50:47 +02:00
} ) ;
2017-05-21 09:25:49 +02:00
if ( result . Count > 0 )
2016-05-04 22:50:47 +02:00
{
return true ;
}
}
}
return false ;
}
2019-12-04 21:39:27 +01:00
/// <inheritdoc />
2015-07-20 20:32:55 +02:00
public void Dispose ( )
{
2019-12-04 21:39:27 +01:00
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
protected virtual void Dispose ( bool disposing )
{
if ( _disposed )
{
return ;
}
if ( disposing )
{
_recordingDeleteSemaphore . Dispose ( ) ;
}
2015-07-20 20:32:55 +02:00
foreach ( var pair in _activeRecordings . ToList ( ) )
{
2016-03-01 05:24:42 +01:00
pair . Value . CancellationTokenSource . Cancel ( ) ;
2015-07-20 20:32:55 +02:00
}
2019-12-04 21:39:27 +01:00
_disposed = true ;
2015-07-20 20:32:55 +02:00
}
2015-08-21 21:25:35 +02:00
2019-12-04 21:39:27 +01:00
public IEnumerable < VirtualFolderInfo > GetRecordingFolders ( )
2016-05-04 22:50:47 +02:00
{
var defaultFolder = RecordingPath ;
var defaultName = "Recordings" ;
2019-01-26 22:59:53 +01:00
if ( Directory . Exists ( defaultFolder ) )
2016-05-04 22:50:47 +02:00
{
2019-12-04 21:39:27 +01:00
yield return new VirtualFolderInfo
2016-05-04 22:50:47 +02:00
{
2017-08-19 21:43:35 +02:00
Locations = new string [ ] { defaultFolder } ,
2016-05-04 22:50:47 +02:00
Name = defaultName
2019-12-04 21:39:27 +01:00
} ;
2016-05-04 22:50:47 +02:00
}
var customPath = GetConfiguration ( ) . MovieRecordingPath ;
2019-12-04 21:39:27 +01:00
if ( ! string . IsNullOrWhiteSpace ( customPath ) & & ! string . Equals ( customPath , defaultFolder , StringComparison . OrdinalIgnoreCase ) & & Directory . Exists ( customPath ) )
2016-05-04 22:50:47 +02:00
{
2019-12-04 21:39:27 +01:00
yield return new VirtualFolderInfo
2016-05-04 22:50:47 +02:00
{
2017-08-19 21:43:35 +02:00
Locations = new string [ ] { customPath } ,
2016-05-04 22:50:47 +02:00
Name = "Recorded Movies" ,
2021-02-24 02:34:50 +01:00
CollectionType = CollectionTypeOptions . Movies
2019-12-04 21:39:27 +01:00
} ;
2016-05-04 22:50:47 +02:00
}
customPath = GetConfiguration ( ) . SeriesRecordingPath ;
2019-12-04 21:39:27 +01:00
if ( ! string . IsNullOrWhiteSpace ( customPath ) & & ! string . Equals ( customPath , defaultFolder , StringComparison . OrdinalIgnoreCase ) & & Directory . Exists ( customPath ) )
2016-05-04 22:50:47 +02:00
{
2019-12-04 21:39:27 +01:00
yield return new VirtualFolderInfo
2016-05-04 22:50:47 +02:00
{
2017-08-19 21:43:35 +02:00
Locations = new string [ ] { customPath } ,
2017-03-28 19:30:47 +02:00
Name = "Recorded Shows" ,
2021-02-24 02:34:50 +01:00
CollectionType = CollectionTypeOptions . TvShows
2019-12-04 21:39:27 +01:00
} ;
2016-05-04 22:50:47 +02:00
}
}
2017-03-15 20:57:18 +01:00
public async Task < List < TunerHostInfo > > DiscoverTuners ( bool newDevicesOnly , CancellationToken cancellationToken )
2017-03-13 21:42:21 +01:00
{
var list = new List < TunerHostInfo > ( ) ;
2017-03-15 20:57:18 +01:00
var configuredDeviceIds = GetConfiguration ( ) . TunerHosts
. Where ( i = > ! string . IsNullOrWhiteSpace ( i . DeviceId ) )
. Select ( i = > i . DeviceId )
. ToList ( ) ;
2017-03-13 21:42:21 +01:00
foreach ( var host in _liveTvManager . TunerHosts )
{
2017-03-14 20:44:35 +01:00
var discoveredDevices = await DiscoverDevices ( host , TunerDiscoveryDurationMs , cancellationToken ) . ConfigureAwait ( false ) ;
2017-03-13 21:42:21 +01:00
2017-03-15 20:57:18 +01:00
if ( newDevicesOnly )
{
discoveredDevices = discoveredDevices . Where ( d = > ! configuredDeviceIds . Contains ( d . DeviceId , StringComparer . OrdinalIgnoreCase ) )
. ToList ( ) ;
}
2019-12-04 21:39:27 +01:00
2017-03-13 21:42:21 +01:00
list . AddRange ( discoveredDevices ) ;
}
return list ;
}
2017-03-13 05:49:10 +01:00
public async Task ScanForTunerDeviceChanges ( CancellationToken cancellationToken )
{
foreach ( var host in _liveTvManager . TunerHosts )
{
await ScanForTunerDeviceChanges ( host , cancellationToken ) . ConfigureAwait ( false ) ;
}
}
private async Task ScanForTunerDeviceChanges ( ITunerHost host , CancellationToken cancellationToken )
{
2017-03-14 20:44:35 +01:00
var discoveredDevices = await DiscoverDevices ( host , TunerDiscoveryDurationMs , cancellationToken ) . ConfigureAwait ( false ) ;
2017-03-13 05:49:10 +01:00
var configuredDevices = GetConfiguration ( ) . TunerHosts
. Where ( i = > string . Equals ( i . Type , host . Type , StringComparison . OrdinalIgnoreCase ) )
. ToList ( ) ;
foreach ( var device in discoveredDevices )
{
var configuredDevice = configuredDevices . FirstOrDefault ( i = > string . Equals ( i . DeviceId , device . DeviceId , StringComparison . OrdinalIgnoreCase ) ) ;
2019-01-07 20:10:26 +01:00
if ( configuredDevice ! = null & & ! string . Equals ( device . Url , configuredDevice . Url , StringComparison . OrdinalIgnoreCase ) )
2017-03-13 05:49:10 +01:00
{
2019-01-07 20:35:26 +01:00
_logger . LogInformation ( "Tuner url has changed from {PreviousUrl} to {NewUrl}" , configuredDevice . Url , device . Url ) ;
2017-03-13 05:49:10 +01:00
2019-01-07 20:10:26 +01:00
configuredDevice . Url = device . Url ;
await _liveTvManager . SaveTunerHost ( configuredDevice ) . ConfigureAwait ( false ) ;
2017-03-13 05:49:10 +01:00
}
}
}
2019-12-04 21:39:27 +01:00
private async Task < List < TunerHostInfo > > DiscoverDevices ( ITunerHost host , int discoveryDurationMs , CancellationToken cancellationToken )
2017-03-13 05:49:10 +01:00
{
try
{
2019-12-04 21:39:27 +01:00
var discoveredDevices = await host . DiscoverDevices ( discoveryDurationMs , cancellationToken ) . ConfigureAwait ( false ) ;
2017-03-13 05:49:10 +01:00
foreach ( var device in discoveredDevices )
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Discovered tuner device {0} at {1}" , host . Name , device . Url ) ;
2017-03-13 05:49:10 +01:00
}
return discoveredDevices ;
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error discovering tuner devices" ) ;
2017-03-13 05:49:10 +01:00
return new List < TunerHostInfo > ( ) ;
}
}
2015-07-20 20:32:55 +02:00
}
2018-12-13 14:18:25 +01:00
}