2015-08-02 23:47:31 +00:00
using MediaBrowser.Common ;
2015-07-20 18:32:55 +00:00
using MediaBrowser.Common.Configuration ;
using MediaBrowser.Common.Net ;
2015-08-21 19:25:35 +00:00
using MediaBrowser.Common.Security ;
2015-08-22 19:46:55 +00:00
using MediaBrowser.Controller.Configuration ;
2015-07-20 18:32:55 +00:00
using MediaBrowser.Controller.Drawing ;
2015-08-22 19:46:55 +00:00
using MediaBrowser.Controller.FileOrganization ;
using MediaBrowser.Controller.Library ;
2015-07-20 18:32:55 +00:00
using MediaBrowser.Controller.LiveTv ;
2015-09-21 16:26:05 +00:00
using MediaBrowser.Controller.MediaEncoding ;
2015-08-22 19:46:55 +00:00
using MediaBrowser.Controller.Providers ;
2015-07-20 18:32:55 +00:00
using MediaBrowser.Model.Dto ;
2015-08-21 19:25:35 +00:00
using MediaBrowser.Model.Entities ;
2015-07-20 18:32:55 +00:00
using MediaBrowser.Model.Events ;
using MediaBrowser.Model.LiveTv ;
using MediaBrowser.Model.Logging ;
using MediaBrowser.Model.Serialization ;
2015-08-22 19:46:55 +00:00
using MediaBrowser.Server.Implementations.FileOrganization ;
2015-07-20 18:32:55 +00:00
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
2016-01-26 18:18:54 +00:00
using System.Globalization ;
2015-07-20 18:32:55 +00:00
using System.IO ;
using System.Linq ;
2016-09-15 23:19:27 +00:00
using System.Text ;
2015-07-20 18:32:55 +00:00
using System.Threading ;
using System.Threading.Tasks ;
2016-09-15 23:19:27 +00:00
using System.Xml ;
2015-10-04 04:23:11 +00:00
using CommonIO ;
2016-10-11 06:46:59 +00:00
using MediaBrowser.Common.Events ;
2016-09-20 19:38:53 +00:00
using MediaBrowser.Common.Extensions ;
2016-10-09 07:18:43 +00:00
using MediaBrowser.Controller ;
2016-05-04 20:50:47 +00:00
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Entities.TV ;
2016-08-13 20:54:29 +00:00
using MediaBrowser.Model.Configuration ;
2016-09-15 23:19:27 +00:00
using MediaBrowser.Model.FileOrganization ;
2016-01-27 06:07:01 +00:00
using Microsoft.Win32 ;
2015-07-20 18:32:55 +00:00
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
2016-10-05 07:15:29 +00:00
public class EmbyTV : ILiveTvService , ISupportsDirectStreamProvider , ISupportsNewTimerIds , IDisposable
2015-07-20 18:32:55 +00:00
{
2016-10-09 07:18:43 +00:00
private readonly IServerApplicationHost _appHost ;
2015-07-20 18:32:55 +00:00
private readonly ILogger _logger ;
private readonly IHttpClient _httpClient ;
2015-08-22 19:46:55 +00:00
private readonly IServerConfigurationManager _config ;
2015-07-20 18:32:55 +00:00
private readonly IJsonSerializer _jsonSerializer ;
private readonly ItemDataProvider < SeriesTimerInfo > _seriesTimerProvider ;
private readonly TimerManager _timerProvider ;
2015-07-23 13:23:22 +00:00
private readonly LiveTvManager _liveTvManager ;
2015-08-16 18:54:25 +00:00
private readonly IFileSystem _fileSystem ;
2015-07-23 05:25:55 +00:00
2015-08-22 19:46:55 +00:00
private readonly ILibraryMonitor _libraryMonitor ;
private readonly ILibraryManager _libraryManager ;
private readonly IProviderManager _providerManager ;
private readonly IFileOrganizationService _organizationService ;
2015-09-21 16:26:05 +00:00
private readonly IMediaEncoder _mediaEncoder ;
2015-08-22 19:46:55 +00:00
2015-07-29 03:42:03 +00:00
public static EmbyTV Current ;
2016-10-11 06:46:59 +00:00
public event EventHandler DataSourceChanged ;
public event EventHandler < RecordingStatusChangedEventArgs > RecordingStatusChanged ;
2016-05-04 20:50:47 +00:00
private readonly ConcurrentDictionary < string , ActiveRecordingInfo > _activeRecordings =
new ConcurrentDictionary < string , ActiveRecordingInfo > ( StringComparer . OrdinalIgnoreCase ) ;
2016-10-09 07:18:43 +00:00
public EmbyTV ( IServerApplicationHost appHost , ILogger logger , IJsonSerializer jsonSerializer , IHttpClient httpClient , IServerConfigurationManager config , ILiveTvManager liveTvManager , IFileSystem fileSystem , ILibraryManager libraryManager , ILibraryMonitor libraryMonitor , IProviderManager providerManager , IFileOrganizationService organizationService , IMediaEncoder mediaEncoder )
2015-07-20 18:32:55 +00:00
{
2015-07-29 03:42:03 +00:00
Current = this ;
2016-10-09 07:18:43 +00:00
_appHost = appHost ;
2015-07-20 18:32:55 +00:00
_logger = logger ;
_httpClient = httpClient ;
_config = config ;
2015-08-16 18:54:25 +00:00
_fileSystem = fileSystem ;
2015-08-22 19:46:55 +00:00
_libraryManager = libraryManager ;
_libraryMonitor = libraryMonitor ;
_providerManager = providerManager ;
_organizationService = organizationService ;
2015-09-21 16:26:05 +00:00
_mediaEncoder = mediaEncoder ;
2015-07-23 13:23:22 +00:00
_liveTvManager = ( LiveTvManager ) liveTvManager ;
2015-07-20 18:32:55 +00:00
_jsonSerializer = jsonSerializer ;
2015-09-21 16:26:05 +00:00
_seriesTimerProvider = new SeriesTimerManager ( fileSystem , jsonSerializer , _logger , Path . Combine ( DataPath , "seriestimers" ) ) ;
2016-09-12 21:24:05 +00:00
_timerProvider = new TimerManager ( fileSystem , jsonSerializer , _logger , Path . Combine ( DataPath , "timers" ) , _logger ) ;
2015-07-20 18:32:55 +00:00
_timerProvider . TimerFired + = _timerProvider_TimerFired ;
2016-05-04 20:50:47 +00:00
_config . NamedConfigurationUpdated + = _config_NamedConfigurationUpdated ;
}
private void _config_NamedConfigurationUpdated ( object sender , ConfigurationUpdateEventArgs e )
{
if ( string . Equals ( e . Key , "livetv" , StringComparison . OrdinalIgnoreCase ) )
{
OnRecordingFoldersChanged ( ) ;
}
2015-07-20 18:32:55 +00:00
}
2015-07-29 03:42:03 +00:00
public void Start ( )
{
_timerProvider . RestartTimers ( ) ;
2016-01-27 06:07:01 +00:00
SystemEvents . PowerModeChanged + = SystemEvents_PowerModeChanged ;
2016-05-23 04:47:02 +00:00
CreateRecordingFolders ( ) ;
2016-05-04 20:50:47 +00:00
}
private void OnRecordingFoldersChanged ( )
{
CreateRecordingFolders ( ) ;
}
2016-05-22 17:07:30 +00:00
internal void CreateRecordingFolders ( )
{
try
{
CreateRecordingFoldersInternal ( ) ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error creating recording folders" , ex ) ;
}
}
internal void CreateRecordingFoldersInternal ( )
2016-05-04 20:50:47 +00:00
{
var recordingFolders = GetRecordingFolders ( ) ;
var virtualFolders = _libraryManager . GetVirtualFolders ( )
. ToList ( ) ;
var allExistingPaths = virtualFolders . SelectMany ( i = > i . Locations ) . ToList ( ) ;
2016-05-20 15:57:07 +00:00
var pathsAdded = new List < string > ( ) ;
2016-05-04 20:50:47 +00:00
foreach ( var recordingFolder in recordingFolders )
{
var pathsToCreate = recordingFolder . Locations
. Where ( i = > ! allExistingPaths . Contains ( i , StringComparer . OrdinalIgnoreCase ) )
. ToList ( ) ;
if ( pathsToCreate . Count = = 0 )
{
continue ;
}
2016-09-23 06:21:54 +00:00
var mediaPathInfos = pathsToCreate . Select ( i = > new MediaPathInfo { Path = i } ) . ToArray ( ) ;
var libraryOptions = new LibraryOptions
{
PathInfos = mediaPathInfos
} ;
2016-05-04 20:50:47 +00:00
try
{
2016-09-23 06:21:54 +00:00
_libraryManager . AddVirtualFolder ( recordingFolder . Name , recordingFolder . CollectionType , libraryOptions , true ) ;
2016-05-04 20:50:47 +00:00
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error creating virtual folder" , ex ) ;
}
2016-05-20 15:57:07 +00:00
pathsAdded . AddRange ( pathsToCreate ) ;
}
var config = GetConfiguration ( ) ;
var pathsToRemove = config . MediaLocationsCreated
. Except ( recordingFolders . SelectMany ( i = > i . Locations ) )
. ToList ( ) ;
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 ) ;
}
foreach ( var path in pathsToRemove )
{
RemovePathFromLibrary ( path ) ;
2016-05-04 20:50:47 +00:00
}
}
private void RemovePathFromLibrary ( string path )
{
2016-05-20 15:57:07 +00:00
_logger . Debug ( "Removing path from library: {0}" , path ) ;
2016-05-04 20:50:47 +00:00
var requiresRefresh = false ;
var virtualFolders = _libraryManager . GetVirtualFolders ( )
. ToList ( ) ;
foreach ( var virtualFolder in virtualFolders )
{
if ( ! virtualFolder . Locations . Contains ( path , StringComparer . OrdinalIgnoreCase ) )
{
continue ;
}
if ( virtualFolder . Locations . Count = = 1 )
{
// remove entire virtual folder
try
{
_libraryManager . RemoveVirtualFolder ( virtualFolder . Name , true ) ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error removing virtual folder" , ex ) ;
}
}
else
{
try
{
_libraryManager . RemoveMediaPath ( virtualFolder . Name , path ) ;
requiresRefresh = true ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error removing media path" , ex ) ;
}
}
}
if ( requiresRefresh )
{
_libraryManager . ValidateMediaLibrary ( new Progress < Double > ( ) , CancellationToken . None ) ;
}
2016-01-27 06:07:01 +00:00
}
void SystemEvents_PowerModeChanged ( object sender , PowerModeChangedEventArgs e )
{
2016-01-27 16:58:08 +00:00
_logger . Info ( "Power mode changed to {0}" , e . Mode ) ;
2016-01-27 06:07:01 +00:00
if ( e . Mode = = PowerModes . Resume )
{
_timerProvider . RestartTimers ( ) ;
}
2015-07-29 03:42:03 +00:00
}
2015-07-20 18:32:55 +00:00
public string Name
{
get { return "Emby" ; }
}
public string DataPath
{
get { return Path . Combine ( _config . CommonApplicationPaths . DataPath , "livetv" ) ; }
}
2016-05-04 20:50:47 +00:00
private string DefaultRecordingPath
{
get
{
return Path . Combine ( DataPath , "recordings" ) ;
}
}
private string RecordingPath
{
get
{
var path = GetConfiguration ( ) . RecordingPath ;
return string . IsNullOrWhiteSpace ( path )
? DefaultRecordingPath
: path ;
}
}
2015-07-20 18:32:55 +00:00
public string HomePageUrl
{
get { return "http://emby.media" ; }
}
public async Task < LiveTvServiceStatusInfo > GetStatusInfoAsync ( CancellationToken cancellationToken )
{
var status = new LiveTvServiceStatusInfo ( ) ;
var list = new List < LiveTvTunerInfo > ( ) ;
2015-08-19 16:43:23 +00:00
foreach ( var hostInstance in _liveTvManager . TunerHosts )
2015-07-20 18:32:55 +00:00
{
2015-07-23 23:40:54 +00:00
try
2015-07-20 18:32:55 +00:00
{
2015-08-19 16:43:23 +00:00
var tuners = await hostInstance . GetTunerInfos ( cancellationToken ) . ConfigureAwait ( false ) ;
2015-07-20 18:32:55 +00:00
2015-07-23 23:40:54 +00:00
list . AddRange ( tuners ) ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error getting tuners" , ex ) ;
2015-07-20 18:32:55 +00:00
}
}
status . Tuners = list ;
status . Status = LiveTvServiceStatus . Ok ;
2016-10-09 07:18:43 +00:00
status . Version = _appHost . ApplicationVersion . ToString ( ) ;
2015-07-21 04:22:46 +00:00
status . IsVisible = false ;
2015-07-20 18:32:55 +00:00
return status ;
}
2015-09-21 16:26:05 +00:00
public async Task RefreshSeriesTimers ( CancellationToken cancellationToken , IProgress < double > progress )
{
2015-11-21 04:34:55 +00:00
var seriesTimers = await GetSeriesTimersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2015-09-21 16:26:05 +00:00
List < ChannelInfo > channels = null ;
2015-11-21 04:34:55 +00:00
foreach ( var timer in seriesTimers )
2015-09-21 16:26:05 +00:00
{
List < ProgramInfo > epgData ;
if ( timer . RecordAnyChannel )
{
if ( channels = = null )
{
channels = ( await GetChannelsAsync ( true , CancellationToken . None ) . ConfigureAwait ( false ) ) . ToList ( ) ;
}
var channelIds = channels . Select ( i = > i . Id ) . ToList ( ) ;
epgData = GetEpgDataForChannels ( channelIds ) ;
}
else
{
epgData = GetEpgDataForChannel ( timer . ChannelId ) ;
}
2016-02-07 19:19:41 +00:00
await UpdateTimersForSeriesTimer ( epgData , timer , true ) . ConfigureAwait ( false ) ;
2015-09-21 16:26:05 +00:00
}
2015-11-21 04:34:55 +00:00
var timers = await GetTimersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
foreach ( var timer in timers . ToList ( ) )
{
if ( DateTime . UtcNow > timer . EndDate & & ! _activeRecordings . ContainsKey ( timer . Id ) )
{
2016-09-26 18:59:18 +00:00
OnTimerOutOfDate ( timer ) ;
2015-11-21 04:34:55 +00:00
}
}
2015-09-21 16:26:05 +00:00
}
2016-09-26 18:59:18 +00:00
private void OnTimerOutOfDate ( TimerInfo timer )
{
_timerProvider . Delete ( timer ) ;
}
2015-08-16 18:54:25 +00:00
private async Task < IEnumerable < ChannelInfo > > GetChannelsAsync ( bool enableCache , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
var list = new List < ChannelInfo > ( ) ;
2015-08-19 16:43:23 +00:00
foreach ( var hostInstance in _liveTvManager . TunerHosts )
2015-07-20 18:32:55 +00:00
{
2015-07-23 23:40:54 +00:00
try
2015-07-20 18:32:55 +00:00
{
2016-09-29 12:55:49 +00:00
var channels = await hostInstance . GetChannels ( enableCache , cancellationToken ) . ConfigureAwait ( false ) ;
2015-07-20 18:32:55 +00:00
2015-08-16 18:37:53 +00:00
list . AddRange ( channels ) ;
2015-07-23 23:40:54 +00:00
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error getting channels" , ex ) ;
2015-07-20 18:32:55 +00:00
}
}
2016-02-24 19:06:26 +00:00
foreach ( var provider in GetListingProviders ( ) )
2015-07-23 05:25:55 +00:00
{
2016-02-24 19:06:26 +00:00
var enabledChannels = list
. Where ( i = > IsListingProviderEnabledForTuner ( provider . Item2 , i . TunerHostId ) )
. ToList ( ) ;
if ( enabledChannels . Count > 0 )
2015-07-23 05:25:55 +00:00
{
2015-08-02 23:47:31 +00:00
try
{
2016-03-16 21:30:49 +00:00
await provider . Item1 . AddMetadata ( provider . Item2 , enabledChannels , cancellationToken ) . ConfigureAwait ( false ) ;
2015-08-02 23:47:31 +00:00
}
catch ( NotSupportedException )
{
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error adding metadata" , ex ) ;
}
2015-07-23 05:25:55 +00:00
}
}
2016-02-24 19:06:26 +00:00
2015-07-20 18:32:55 +00:00
return list ;
}
2016-06-09 16:13:25 +00: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 12:55:49 +00:00
var channels = await hostInstance . GetChannels ( false , cancellationToken ) . ConfigureAwait ( false ) ;
2016-06-09 16:13:25 +00:00
list . AddRange ( channels ) ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error getting channels" , ex ) ;
}
}
return list
. Where ( i = > IsListingProviderEnabledForTuner ( listingsProvider , i . TunerHostId ) )
. ToList ( ) ;
}
2015-08-16 18:54:25 +00:00
public Task < IEnumerable < ChannelInfo > > GetChannelsAsync ( CancellationToken cancellationToken )
{
return GetChannelsAsync ( false , cancellationToken ) ;
}
2015-07-20 18:32:55 +00:00
public Task CancelSeriesTimerAsync ( string timerId , CancellationToken cancellationToken )
{
2016-02-01 00:57:40 +00:00
var timers = _timerProvider
. GetAll ( )
. Where ( i = > string . Equals ( i . SeriesTimerId , timerId , StringComparison . OrdinalIgnoreCase ) )
. ToList ( ) ;
2015-08-17 16:52:56 +00:00
foreach ( var timer in timers )
{
2016-09-26 18:59:18 +00:00
CancelTimerInternal ( timer . Id , true ) ;
2015-08-17 16:52:56 +00:00
}
2015-07-29 03:42:03 +00:00
var remove = _seriesTimerProvider . GetAll ( ) . FirstOrDefault ( r = > string . Equals ( r . Id , timerId , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-20 18:32:55 +00:00
if ( remove ! = null )
{
_seriesTimerProvider . Delete ( remove ) ;
}
return Task . FromResult ( true ) ;
}
2016-09-26 18:59:18 +00:00
private void CancelTimerInternal ( string timerId , bool isSeriesCancelled )
2015-07-20 18:32:55 +00:00
{
2016-09-27 05:13:56 +00:00
var timer = _timerProvider . GetTimer ( timerId ) ;
2016-09-26 18:59:18 +00:00
if ( timer ! = null )
2015-07-20 18:32:55 +00:00
{
2016-09-26 18:59:18 +00:00
if ( string . IsNullOrWhiteSpace ( timer . SeriesTimerId ) | | isSeriesCancelled )
{
_timerProvider . Delete ( timer ) ;
}
else
{
timer . Status = RecordingStatus . Cancelled ;
_timerProvider . AddOrUpdate ( timer , false ) ;
}
2015-07-20 18:32:55 +00:00
}
2016-03-01 04:24:42 +00:00
ActiveRecordingInfo activeRecordingInfo ;
2015-07-20 18:32:55 +00:00
2016-03-01 04:24:42 +00:00
if ( _activeRecordings . TryGetValue ( timerId , out activeRecordingInfo ) )
2015-07-20 18:32:55 +00:00
{
2016-03-01 04:24:42 +00:00
activeRecordingInfo . CancellationTokenSource . Cancel ( ) ;
2015-07-20 18:32:55 +00:00
}
}
public Task CancelTimerAsync ( string timerId , CancellationToken cancellationToken )
{
2016-09-26 18:59:18 +00:00
CancelTimerInternal ( timerId , false ) ;
2015-07-20 18:32:55 +00:00
return Task . FromResult ( true ) ;
}
2016-05-04 20:50:47 +00:00
public Task DeleteRecordingAsync ( string recordingId , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-05-04 20:50:47 +00:00
return Task . FromResult ( true ) ;
2015-07-20 18:32:55 +00:00
}
2016-09-27 05:13:56 +00:00
public Task CreateSeriesTimerAsync ( SeriesTimerInfo info , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
2015-07-20 18:32:55 +00:00
public Task CreateTimerAsync ( TimerInfo info , CancellationToken cancellationToken )
2016-09-27 05:13:56 +00:00
{
throw new NotImplementedException ( ) ;
}
public Task < string > CreateTimer ( TimerInfo timer , CancellationToken cancellationToken )
2016-06-08 21:04:52 +00:00
{
2016-09-26 18:59:18 +00:00
var existingTimer = _timerProvider . GetAll ( )
2016-09-27 05:13:56 +00:00
. FirstOrDefault ( i = > string . Equals ( timer . ProgramId , i . ProgramId , StringComparison . OrdinalIgnoreCase ) ) ;
2016-09-26 18:59:18 +00:00
if ( existingTimer ! = null )
{
2016-09-30 06:50:06 +00:00
if ( existingTimer . Status = = RecordingStatus . Cancelled | |
existingTimer . Status = = RecordingStatus . Completed )
2016-09-26 18:59:18 +00:00
{
existingTimer . Status = RecordingStatus . New ;
_timerProvider . Update ( existingTimer ) ;
2016-09-27 05:13:56 +00:00
return Task . FromResult ( existingTimer . Id ) ;
2016-09-26 18:59:18 +00:00
}
else
{
throw new ArgumentException ( "A scheduled recording already exists for this program." ) ;
}
}
2016-09-15 06:23:39 +00:00
timer . Id = Guid . NewGuid ( ) . ToString ( "N" ) ;
ProgramInfo programInfo = null ;
if ( ! string . IsNullOrWhiteSpace ( timer . ProgramId ) )
{
programInfo = GetProgramInfoFromCache ( timer . ChannelId , timer . ProgramId ) ;
}
if ( programInfo = = null )
{
_logger . Info ( "Unable to find program with Id {0}. Will search using start date" , timer . ProgramId ) ;
programInfo = GetProgramInfoFromCache ( timer . ChannelId , timer . StartDate ) ;
}
if ( programInfo ! = null )
{
RecordingHelper . CopyProgramInfoToTimerInfo ( programInfo , timer ) ;
}
_timerProvider . Add ( timer ) ;
return Task . FromResult ( timer . Id ) ;
2015-07-20 18:32:55 +00:00
}
2016-06-08 21:04:52 +00:00
public async Task < string > CreateSeriesTimer ( SeriesTimerInfo info , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2015-07-29 17:16:00 +00:00
info . Id = Guid . NewGuid ( ) . ToString ( "N" ) ;
2015-07-20 18:32:55 +00:00
2015-08-16 22:03:22 +00:00
List < ProgramInfo > epgData ;
if ( info . RecordAnyChannel )
{
var channels = await GetChannelsAsync ( true , CancellationToken . None ) . ConfigureAwait ( false ) ;
var channelIds = channels . Select ( i = > i . Id ) . ToList ( ) ;
epgData = GetEpgDataForChannels ( channelIds ) ;
}
else
{
epgData = GetEpgDataForChannel ( info . ChannelId ) ;
}
// populate info.seriesID
var program = epgData . FirstOrDefault ( i = > string . Equals ( i . Id , info . ProgramId , StringComparison . OrdinalIgnoreCase ) ) ;
if ( program ! = null )
{
info . SeriesId = program . SeriesId ;
}
else
{
throw new InvalidOperationException ( "SeriesId for program not found" ) ;
}
2015-07-20 18:32:55 +00:00
_seriesTimerProvider . Add ( info ) ;
2016-01-20 03:48:37 +00:00
await UpdateTimersForSeriesTimer ( epgData , info , false ) . ConfigureAwait ( false ) ;
2016-06-08 21:04:52 +00:00
return info . Id ;
2015-07-20 18:32:55 +00:00
}
2015-08-15 21:58:52 +00:00
public async Task UpdateSeriesTimerAsync ( SeriesTimerInfo info , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-01-20 03:48:37 +00:00
var instance = _seriesTimerProvider . GetAll ( ) . FirstOrDefault ( i = > string . Equals ( i . Id , info . Id , StringComparison . OrdinalIgnoreCase ) ) ;
if ( instance ! = null )
2015-08-16 22:03:22 +00:00
{
2016-01-20 03:48:37 +00: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 21:09:14 +00:00
instance . SkipEpisodesInLibrary = info . SkipEpisodesInLibrary ;
instance . KeepUpTo = info . KeepUpTo ;
2016-09-26 18:59:18 +00:00
instance . KeepUntil = info . KeepUntil ;
2016-01-20 03:48:37 +00:00
instance . StartDate = info . StartDate ;
_seriesTimerProvider . Update ( instance ) ;
2015-08-16 22:03:22 +00:00
2016-01-20 03:48:37 +00:00
List < ProgramInfo > epgData ;
if ( instance . RecordAnyChannel )
{
var channels = await GetChannelsAsync ( true , CancellationToken . None ) . ConfigureAwait ( false ) ;
var channelIds = channels . Select ( i = > i . Id ) . ToList ( ) ;
epgData = GetEpgDataForChannels ( channelIds ) ;
}
else
{
epgData = GetEpgDataForChannel ( instance . ChannelId ) ;
}
await UpdateTimersForSeriesTimer ( epgData , instance , true ) . ConfigureAwait ( false ) ;
}
2015-07-20 18:32:55 +00:00
}
2016-09-26 18:59:18 +00:00
public Task UpdateTimerAsync ( TimerInfo updatedTimer , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-09-27 05:13:56 +00:00
var existingTimer = _timerProvider . GetTimer ( updatedTimer . Id ) ;
2016-09-26 18:59:18 +00:00
if ( existingTimer = = null )
{
throw new ResourceNotFoundException ( ) ;
}
// Only update if not currently active
ActiveRecordingInfo activeRecordingInfo ;
if ( ! _activeRecordings . TryGetValue ( updatedTimer . Id , out activeRecordingInfo ) )
{
2016-10-03 06:28:45 +00:00
existingTimer . PrePaddingSeconds = updatedTimer . PrePaddingSeconds ;
existingTimer . PostPaddingSeconds = updatedTimer . PostPaddingSeconds ;
existingTimer . IsPostPaddingRequired = updatedTimer . IsPostPaddingRequired ;
existingTimer . IsPrePaddingRequired = updatedTimer . IsPrePaddingRequired ;
2016-09-26 18:59:18 +00:00
}
2015-07-20 18:32:55 +00:00
return Task . FromResult ( true ) ;
}
2016-10-03 06:28:45 +00:00
private void UpdateExistingTimerWithNewMetadata ( TimerInfo existingTimer , TimerInfo updatedTimer )
2016-09-26 18:59:18 +00: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 . HomePageUrl = updatedTimer . HomePageUrl ;
existingTimer . IsKids = updatedTimer . IsKids ;
2016-09-29 12:55:49 +00:00
existingTimer . IsNews = updatedTimer . IsNews ;
2016-09-26 18:59:18 +00:00
existingTimer . IsMovie = updatedTimer . IsMovie ;
existingTimer . IsProgramSeries = updatedTimer . IsProgramSeries ;
2016-10-04 05:15:39 +00:00
existingTimer . IsRepeat = updatedTimer . IsRepeat ;
2016-09-26 18:59:18 +00:00
existingTimer . IsSports = updatedTimer . IsSports ;
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 . ShortOverview = updatedTimer . ShortOverview ;
existingTimer . StartDate = updatedTimer . StartDate ;
}
2015-07-20 18:32:55 +00:00
public Task < ImageStream > GetChannelImageAsync ( string channelId , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
public Task < ImageStream > GetRecordingImageAsync ( string recordingId , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
public Task < ImageStream > GetProgramImageAsync ( string programId , string channelId , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
2015-10-11 00:39:30 +00:00
public async Task < IEnumerable < RecordingInfo > > GetRecordingsAsync ( CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-10-09 07:18:43 +00:00
return _activeRecordings . Values . ToList ( ) . Select ( GetRecordingInfo ) . ToList ( ) ;
}
public string GetActiveRecordingPath ( string id )
{
ActiveRecordingInfo info ;
if ( _activeRecordings . TryGetValue ( id , out info ) )
{
return info . Path ;
}
return null ;
}
private RecordingInfo GetRecordingInfo ( ActiveRecordingInfo info )
{
var timer = info . Timer ;
var program = info . Program ;
var result = new RecordingInfo
{
ChannelId = timer . ChannelId ,
CommunityRating = timer . CommunityRating ,
DateLastUpdated = DateTime . UtcNow ,
EndDate = timer . EndDate ,
EpisodeTitle = timer . EpisodeTitle ,
Genres = timer . Genres ,
Id = "recording" + timer . Id ,
IsKids = timer . IsKids ,
IsMovie = timer . IsMovie ,
IsNews = timer . IsNews ,
IsRepeat = timer . IsRepeat ,
IsSeries = timer . IsProgramSeries ,
IsSports = timer . IsSports ,
Name = timer . Name ,
OfficialRating = timer . OfficialRating ,
OriginalAirDate = timer . OriginalAirDate ,
Overview = timer . Overview ,
ProgramId = timer . ProgramId ,
SeriesTimerId = timer . SeriesTimerId ,
StartDate = timer . StartDate ,
Status = RecordingStatus . InProgress ,
TimerId = timer . Id
} ;
if ( program ! = null )
{
result . Audio = program . Audio ;
result . ImagePath = program . ImagePath ;
result . ImageUrl = program . ImageUrl ;
result . IsHD = program . IsHD ;
result . IsLive = program . IsLive ;
result . IsPremiere = program . IsPremiere ;
result . ShowId = program . ShowId ;
}
return result ;
2015-07-20 18:32:55 +00:00
}
public Task < IEnumerable < TimerInfo > > GetTimersAsync ( CancellationToken cancellationToken )
{
2016-09-26 18:59:18 +00:00
var excludeStatues = new List < RecordingStatus >
{
2016-10-04 05:15:39 +00:00
RecordingStatus . Completed
2016-09-26 18:59:18 +00:00
} ;
var timers = _timerProvider . GetAll ( )
. Where ( i = > ! excludeStatues . Contains ( i . Status ) ) ;
return Task . FromResult ( timers ) ;
2015-07-20 18:32:55 +00:00
}
public Task < SeriesTimerInfo > GetNewTimerDefaultsAsync ( CancellationToken cancellationToken , ProgramInfo program = null )
{
2015-08-24 02:08:20 +00:00
var config = GetConfiguration ( ) ;
2015-07-20 18:32:55 +00:00
var defaults = new SeriesTimerInfo ( )
{
2015-08-24 02:08:20 +00:00
PostPaddingSeconds = Math . Max ( config . PostPaddingSeconds , 0 ) ,
PrePaddingSeconds = Math . Max ( config . PrePaddingSeconds , 0 ) ,
2016-10-14 16:22:04 +00:00
RecordAnyChannel = false ,
2016-05-29 18:42:39 +00:00
RecordAnyTime = true ,
2016-09-14 16:21:33 +00:00
RecordNewOnly = true ,
2016-05-29 18:42:39 +00:00
Days = new List < DayOfWeek >
{
DayOfWeek . Sunday ,
DayOfWeek . Monday ,
DayOfWeek . Tuesday ,
DayOfWeek . Wednesday ,
DayOfWeek . Thursday ,
DayOfWeek . Friday ,
DayOfWeek . Saturday
}
2015-07-20 18:32:55 +00:00
} ;
2015-08-11 17:47:29 +00:00
if ( program ! = null )
{
defaults . SeriesId = program . SeriesId ;
defaults . ProgramId = program . Id ;
2016-10-13 15:07:21 +00:00
defaults . RecordNewOnly = ! program . IsRepeat ;
2015-08-11 17:47:29 +00:00
}
2016-10-16 17:11:32 +00:00
defaults . SkipEpisodesInLibrary = defaults . RecordNewOnly ;
2016-09-26 18:59:18 +00:00
defaults . KeepUntil = KeepUntil . UntilDeleted ;
2015-07-20 18:32:55 +00:00
return Task . FromResult ( defaults ) ;
}
public Task < IEnumerable < SeriesTimerInfo > > GetSeriesTimersAsync ( CancellationToken cancellationToken )
{
return Task . FromResult ( ( IEnumerable < SeriesTimerInfo > ) _seriesTimerProvider . GetAll ( ) ) ;
}
2015-07-23 05:25:55 +00:00
public async Task < IEnumerable < ProgramInfo > > GetProgramsAsync ( string channelId , DateTime startDateUtc , DateTime endDateUtc , CancellationToken cancellationToken )
2015-09-10 18:28:22 +00:00
{
try
{
return await GetProgramsAsyncInternal ( channelId , startDateUtc , endDateUtc , cancellationToken ) . ConfigureAwait ( false ) ;
}
2015-10-25 18:34:31 +00:00
catch ( OperationCanceledException )
{
throw ;
}
2015-09-10 18:28:22 +00:00
catch ( Exception ex )
{
_logger . ErrorException ( "Error getting programs" , ex ) ;
return GetEpgDataForChannel ( channelId ) . Where ( i = > i . StartDate < = endDateUtc & & i . EndDate > = startDateUtc ) ;
}
}
2016-02-24 19:06:26 +00:00
private bool IsListingProviderEnabledForTuner ( ListingsProviderInfo info , string tunerHostId )
{
2016-03-16 04:14:38 +00:00
if ( info . EnableAllTuners )
{
return true ;
}
if ( string . IsNullOrWhiteSpace ( tunerHostId ) )
{
throw new ArgumentNullException ( "tunerHostId" ) ;
}
return info . EnabledTuners . Contains ( tunerHostId , StringComparer . OrdinalIgnoreCase ) ;
2016-02-24 19:06:26 +00:00
}
2015-09-10 18:28:22 +00:00
private async Task < IEnumerable < ProgramInfo > > GetProgramsAsyncInternal ( string channelId , DateTime startDateUtc , DateTime endDateUtc , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2015-08-16 18:54:25 +00:00
var channels = await GetChannelsAsync ( true , cancellationToken ) . ConfigureAwait ( false ) ;
var channel = channels . First ( i = > string . Equals ( i . Id , channelId , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-23 05:25:55 +00:00
foreach ( var provider in GetListingProviders ( ) )
{
2016-02-24 19:06:26 +00:00
if ( ! IsListingProviderEnabledForTuner ( provider . Item2 , channel . TunerHostId ) )
{
2016-03-17 19:29:53 +00:00
_logger . Debug ( "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 19:06:26 +00:00
continue ;
}
2016-03-17 19:29:53 +00:00
_logger . Debug ( "Getting programs for channel {0}-{1} from {2}-{3}" , channel . Number , channel . Name , provider . Item1 . Name , provider . Item2 . ListingsId ? ? string . Empty ) ;
2016-03-23 19:06:36 +00:00
2016-06-06 18:22:42 +00:00
var channelMappings = GetChannelMappings ( provider . Item2 ) ;
var channelNumber = channel . Number ;
string mappedChannelNumber ;
if ( channelMappings . TryGetValue ( channelNumber , out mappedChannelNumber ) )
{
_logger . Debug ( "Found mapped channel on provider {0}. Tuner channel number: {1}, Mapped channel number: {2}" , provider . Item1 . Name , channelNumber , mappedChannelNumber ) ;
channelNumber = mappedChannelNumber ;
}
var programs = await provider . Item1 . GetProgramsAsync ( provider . Item2 , channelNumber , channel . Name , startDateUtc , endDateUtc , cancellationToken )
2015-07-23 05:25:55 +00:00
. ConfigureAwait ( false ) ;
2015-09-10 18:28:22 +00:00
2015-07-23 05:25:55 +00:00
var list = programs . ToList ( ) ;
2015-07-23 23:40:54 +00:00
// Replace the value that came from the provider with a normalized value
foreach ( var program in list )
{
program . ChannelId = channelId ;
}
2015-07-23 05:25:55 +00:00
if ( list . Count > 0 )
{
2015-07-29 03:42:03 +00:00
SaveEpgDataForChannel ( channelId , list ) ;
2015-07-23 05:25:55 +00:00
return list ;
}
}
return new List < ProgramInfo > ( ) ;
}
2016-06-06 18:22:42 +00:00
private Dictionary < string , string > GetChannelMappings ( ListingsProviderInfo info )
{
var dict = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
foreach ( var mapping in info . ChannelMappings )
{
dict [ mapping . Name ] = mapping . Value ;
}
return dict ;
}
2015-07-23 05:25:55 +00:00
private List < Tuple < IListingsProvider , ListingsProviderInfo > > GetListingProviders ( )
{
return GetConfiguration ( ) . ListingProviders
. Select ( i = >
{
2015-07-23 13:23:22 +00:00
var provider = _liveTvManager . ListingProviders . FirstOrDefault ( l = > string . Equals ( l . Type , i . Type , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-23 05:25:55 +00:00
return provider = = null ? null : new Tuple < IListingsProvider , ListingsProviderInfo > ( provider , i ) ;
} )
. Where ( i = > i ! = null )
. ToList ( ) ;
2015-07-20 18:32:55 +00:00
}
public Task < MediaSourceInfo > GetRecordingStream ( string recordingId , string streamId , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
2016-09-25 18:39:13 +00:00
private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim ( 1 , 1 ) ;
2016-10-10 18:18:28 +00:00
private readonly List < LiveStream > _liveStreams = new List < LiveStream > ( ) ;
2016-09-25 18:39:13 +00:00
2015-07-23 23:40:54 +00:00
public async Task < MediaSourceInfo > GetChannelStream ( string channelId , string streamId , CancellationToken cancellationToken )
2016-10-05 07:15:29 +00:00
{
var result = await GetChannelStreamWithDirectStreamProvider ( channelId , streamId , cancellationToken ) . ConfigureAwait ( false ) ;
return result . Item1 ;
}
public async Task < Tuple < MediaSourceInfo , IDirectStreamProvider > > GetChannelStreamWithDirectStreamProvider ( string channelId , string streamId , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-09-25 18:39:13 +00:00
var result = await GetChannelStreamInternal ( channelId , streamId , cancellationToken ) . ConfigureAwait ( false ) ;
2015-07-23 23:40:54 +00:00
2016-10-05 07:15:29 +00:00
return new Tuple < MediaSourceInfo , IDirectStreamProvider > ( result . Item2 , result . Item1 as IDirectStreamProvider ) ;
2016-09-29 12:55:49 +00:00
}
2016-10-07 15:08:13 +00:00
private MediaSourceInfo CloneMediaSource ( MediaSourceInfo mediaSource , bool enableStreamSharing )
2016-09-29 12:55:49 +00:00
{
var json = _jsonSerializer . SerializeToString ( mediaSource ) ;
mediaSource = _jsonSerializer . DeserializeFromString < MediaSourceInfo > ( json ) ;
2016-09-30 06:50:06 +00:00
mediaSource . Id = Guid . NewGuid ( ) . ToString ( "N" ) + "_" + mediaSource . Id ;
2016-10-07 15:08:13 +00:00
//if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
//{
// var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
// ticks = Math.Max(0, ticks);
// mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
//}
return mediaSource ;
}
public async Task < LiveStream > GetLiveStream ( string uniqueId )
{
await _liveStreamsSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
try
{
2016-10-10 18:18:28 +00:00
return _liveStreams
2016-10-07 15:08:13 +00:00
. FirstOrDefault ( i = > string . Equals ( i . UniqueId , uniqueId , StringComparison . OrdinalIgnoreCase ) ) ;
}
finally
2016-09-30 06:50:06 +00:00
{
2016-10-07 15:08:13 +00:00
_liveStreamsSemaphore . Release ( ) ;
2016-09-30 06:50:06 +00:00
}
2016-09-29 12:55:49 +00:00
2015-10-16 18:11:11 +00:00
}
2016-09-29 12:55:49 +00:00
private async Task < Tuple < LiveStream , MediaSourceInfo , ITunerHost > > GetChannelStreamInternal ( string channelId , string streamId , CancellationToken cancellationToken )
2015-10-16 18:11:11 +00:00
{
_logger . Info ( "Streaming Channel " + channelId ) ;
2016-09-29 12:55:49 +00:00
await _liveStreamsSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2016-10-10 18:18:28 +00:00
var result = _liveStreams . FirstOrDefault ( i = > string . Equals ( i . OriginalStreamId , streamId , StringComparison . OrdinalIgnoreCase ) ) ;
2016-09-29 12:55:49 +00:00
2016-09-30 06:50:06 +00:00
if ( result ! = null & & result . EnableStreamSharing )
2015-07-23 23:40:54 +00:00
{
2016-10-10 18:18:28 +00:00
var openedMediaSource = CloneMediaSource ( result . OpenedMediaSource , result . EnableStreamSharing ) ;
result . SharedStreamIds . Add ( openedMediaSource . Id ) ;
_liveStreamsSemaphore . Release ( ) ;
2016-04-26 02:16:46 +00:00
2016-09-30 06:50:06 +00:00
_logger . Info ( "Live stream {0} consumer count is now {1}" , streamId , result . ConsumerCount ) ;
2016-09-25 18:39:13 +00:00
2016-09-30 06:50:06 +00:00
return new Tuple < LiveStream , MediaSourceInfo , ITunerHost > ( result , openedMediaSource , result . TunerHost ) ;
2016-09-29 12:55:49 +00:00
}
try
{
foreach ( var hostInstance in _liveTvManager . TunerHosts )
2015-07-23 23:40:54 +00:00
{
2016-09-29 12:55:49 +00:00
try
{
result = await hostInstance . GetChannelStream ( channelId , streamId , cancellationToken ) . ConfigureAwait ( false ) ;
2016-10-07 15:08:13 +00:00
var openedMediaSource = CloneMediaSource ( result . OpenedMediaSource , result . EnableStreamSharing ) ;
2016-09-30 06:50:06 +00:00
2016-10-10 18:18:28 +00:00
result . SharedStreamIds . Add ( openedMediaSource . Id ) ;
_liveStreams . Add ( result ) ;
2016-09-29 12:55:49 +00:00
result . TunerHost = hostInstance ;
result . OriginalStreamId = streamId ;
_logger . Info ( "Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}" ,
2016-09-30 06:50:06 +00:00
streamId , openedMediaSource . Id , openedMediaSource . LiveStreamId ) ;
2016-09-29 12:55:49 +00:00
2016-09-30 06:50:06 +00:00
return new Tuple < LiveStream , MediaSourceInfo , ITunerHost > ( result , openedMediaSource , hostInstance ) ;
2016-09-29 12:55:49 +00:00
}
catch ( FileNotFoundException )
{
}
catch ( OperationCanceledException )
{
}
2015-07-23 23:40:54 +00:00
}
}
2016-09-29 12:55:49 +00:00
finally
{
_liveStreamsSemaphore . Release ( ) ;
}
2015-07-23 23:40:54 +00:00
throw new ApplicationException ( "Tuner not found." ) ;
2015-07-20 18:32:55 +00:00
}
2015-07-23 23:40:54 +00:00
public async Task < List < MediaSourceInfo > > GetChannelStreamMediaSources ( string channelId , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2015-08-19 19:25:18 +00:00
foreach ( var hostInstance in _liveTvManager . TunerHosts )
2015-07-23 23:40:54 +00:00
{
2015-08-16 18:37:53 +00:00
try
2015-07-23 23:40:54 +00:00
{
2015-08-19 19:25:18 +00:00
var sources = await hostInstance . GetChannelStreamMediaSources ( channelId , cancellationToken ) . ConfigureAwait ( false ) ;
2015-07-23 23:40:54 +00:00
2015-08-16 18:37:53 +00:00
if ( sources . Count > 0 )
{
return sources ;
}
}
catch ( NotImplementedException )
{
2015-08-16 18:54:25 +00:00
2015-07-23 23:40:54 +00:00
}
}
2015-08-27 19:59:42 +00:00
throw new NotImplementedException ( ) ;
2015-07-20 18:32:55 +00:00
}
2016-10-11 06:46:59 +00:00
public async Task < List < MediaSourceInfo > > GetRecordingStreamMediaSources ( string recordingId , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-10-09 07:18:43 +00:00
ActiveRecordingInfo info ;
recordingId = recordingId . Replace ( "recording" , string . Empty ) ;
if ( _activeRecordings . TryGetValue ( recordingId , out info ) )
{
2016-10-11 06:46:59 +00:00
var stream = new MediaSourceInfo
2016-10-09 07:18:43 +00:00
{
2016-10-19 20:31:46 +00:00
Path = _appHost . GetLocalApiUrl ( "127.0.0.1" ) + "/LiveTv/LiveRecordings/" + recordingId + "/stream" ,
2016-10-11 06:46:59 +00:00
Id = recordingId ,
SupportsDirectPlay = false ,
SupportsDirectStream = true ,
SupportsTranscoding = true ,
IsInfiniteStream = true ,
RequiresOpening = false ,
RequiresClosing = false ,
Protocol = Model . MediaInfo . MediaProtocol . Http ,
BufferMs = 0
} ;
var isAudio = false ;
await new LiveStreamHelper ( _mediaEncoder , _logger ) . AddMediaInfoWithProbe ( stream , isAudio , cancellationToken ) . ConfigureAwait ( false ) ;
return new List < MediaSourceInfo >
{
stream
} ;
2016-10-09 07:18:43 +00:00
}
throw new FileNotFoundException ( ) ;
2015-07-20 18:32:55 +00:00
}
2016-09-25 18:39:13 +00:00
public async Task CloseLiveStream ( string id , CancellationToken cancellationToken )
2015-07-20 18:32:55 +00:00
{
2016-09-29 12:55:49 +00:00
// Ignore the consumer id
2016-09-30 06:50:06 +00:00
//id = id.Substring(id.IndexOf('_') + 1);
2016-09-29 12:55:49 +00:00
await _liveStreamsSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2016-09-25 18:39:13 +00:00
try
{
2016-10-10 18:18:28 +00:00
var stream = _liveStreams . FirstOrDefault ( i = > i . SharedStreamIds . Contains ( id ) ) ;
if ( stream ! = null )
2016-09-25 18:39:13 +00:00
{
2016-10-10 18:18:28 +00:00
stream . SharedStreamIds . Remove ( id ) ;
2016-09-25 18:39:13 +00:00
2016-09-29 12:55:49 +00:00
_logger . Info ( "Live stream {0} consumer count is now {1}" , id , stream . ConsumerCount ) ;
if ( stream . ConsumerCount < = 0 )
2016-09-25 18:39:13 +00:00
{
2016-10-10 18:18:28 +00:00
_liveStreams . Remove ( stream ) ;
2016-09-29 12:55:49 +00:00
_logger . Info ( "Closing live stream {0}" , id ) ;
2016-09-25 18:39:13 +00:00
await stream . Close ( ) . ConfigureAwait ( false ) ;
2016-09-27 17:51:01 +00:00
_logger . Info ( "Live stream {0} closed successfully" , id ) ;
2016-09-25 18:39:13 +00:00
}
}
2016-09-29 12:55:49 +00:00
else
{
_logger . Warn ( "Live stream not found: {0}, unable to close" , id ) ;
}
}
catch ( OperationCanceledException )
{
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error closing live stream" , ex ) ;
2016-09-25 18:39:13 +00:00
}
finally
{
_liveStreamsSemaphore . Release ( ) ;
}
2015-07-20 18:32:55 +00:00
}
public Task RecordLiveStream ( string id , CancellationToken cancellationToken )
{
return Task . FromResult ( 0 ) ;
}
public Task ResetTuner ( string id , CancellationToken cancellationToken )
{
return Task . FromResult ( 0 ) ;
}
async void _timerProvider_TimerFired ( object sender , GenericEventArgs < TimerInfo > e )
{
2015-08-22 18:29:12 +00:00
var timer = e . Argument ;
2015-08-31 23:47:47 +00:00
_logger . Info ( "Recording timer fired." ) ;
2015-09-21 16:23:20 +00:00
2015-07-20 18:32:55 +00:00
try
{
2015-09-26 02:31:13 +00:00
var recordingEndDate = timer . EndDate . AddSeconds ( timer . PostPaddingSeconds ) ;
if ( recordingEndDate < = DateTime . UtcNow )
{
2016-09-26 18:59:18 +00:00
_logger . Warn ( "Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended." , timer . Name , timer . Id ) ;
OnTimerOutOfDate ( timer ) ;
2015-09-26 02:31:13 +00:00
return ;
}
2016-03-01 04:24:42 +00:00
var activeRecordingInfo = new ActiveRecordingInfo
{
CancellationTokenSource = new CancellationTokenSource ( ) ,
2016-10-09 07:18:43 +00:00
Timer = timer
2016-03-01 04:24:42 +00:00
} ;
2015-07-20 18:32:55 +00:00
2016-03-01 04:24:42 +00:00
if ( _activeRecordings . TryAdd ( timer . Id , activeRecordingInfo ) )
2015-07-20 18:32:55 +00:00
{
2016-03-01 04:24:42 +00:00
await RecordStream ( timer , recordingEndDate , activeRecordingInfo , activeRecordingInfo . CancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
2015-07-20 18:32:55 +00:00
}
2016-01-26 18:18:54 +00:00
else
{
_logger . Info ( "Skipping RecordStream because it's already in progress." ) ;
}
2015-07-20 18:32:55 +00:00
}
catch ( OperationCanceledException )
{
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error recording stream" , ex ) ;
}
}
2016-09-15 23:19:27 +00:00
private string GetRecordingPath ( TimerInfo timer , out string seriesPath )
2015-07-20 18:32:55 +00:00
{
var recordPath = RecordingPath ;
2016-05-04 20:50:47 +00:00
var config = GetConfiguration ( ) ;
2016-09-15 23:19:27 +00:00
seriesPath = null ;
2015-08-20 23:55:23 +00:00
2016-09-15 06:23:39 +00:00
if ( timer . IsProgramSeries )
2015-08-20 23:55:23 +00:00
{
2016-05-04 20:50:47 +00:00
var customRecordingPath = config . SeriesRecordingPath ;
2016-05-09 03:13:38 +00:00
var allowSubfolder = true ;
if ( ! string . IsNullOrWhiteSpace ( customRecordingPath ) )
{
allowSubfolder = string . Equals ( customRecordingPath , recordPath , StringComparison . OrdinalIgnoreCase ) ;
recordPath = customRecordingPath ;
}
if ( allowSubfolder & & config . EnableRecordingSubfolders )
2016-05-04 20:50:47 +00:00
{
recordPath = Path . Combine ( recordPath , "Series" ) ;
}
2016-09-15 06:23:39 +00:00
var folderName = _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ;
2016-05-04 20:50:47 +00:00
2016-09-15 06:38:09 +00: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 04:17:57 +00:00
2016-09-15 23:19:27 +00:00
seriesPath = recordPath ;
2016-09-15 06:23:39 +00:00
if ( timer . SeasonNumber . HasValue )
2016-05-03 04:17:57 +00:00
{
2016-09-15 06:23:39 +00:00
folderName = string . Format ( "Season {0}" , timer . SeasonNumber . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-05-03 04:17:57 +00:00
recordPath = Path . Combine ( recordPath , folderName ) ;
}
2015-08-20 23:55:23 +00:00
}
2016-09-15 06:23:39 +00:00
else if ( timer . IsMovie )
2016-08-31 19:17:11 +00: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 06:23:39 +00:00
var folderName = _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ;
if ( timer . ProductionYear . HasValue )
2016-08-31 19:17:11 +00:00
{
2016-09-15 06:23:39 +00:00
folderName + = " (" + timer . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) + ")" ;
2016-08-31 19:17:11 +00:00
}
recordPath = Path . Combine ( recordPath , folderName ) ;
}
2016-09-15 06:23:39 +00:00
else if ( timer . IsKids )
2015-08-20 23:55:23 +00:00
{
2016-05-04 20:50:47 +00:00
if ( config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Kids" ) ;
}
2016-09-15 06:23:39 +00:00
var folderName = _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ;
if ( timer . ProductionYear . HasValue )
2016-05-04 20:50:47 +00:00
{
2016-09-15 06:23:39 +00:00
folderName + = " (" + timer . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) + ")" ;
2016-05-04 20:50:47 +00:00
}
recordPath = Path . Combine ( recordPath , folderName ) ;
2015-08-20 23:55:23 +00:00
}
2016-09-15 06:23:39 +00:00
else if ( timer . IsSports )
2015-08-20 23:55:23 +00:00
{
2016-05-04 20:50:47 +00:00
if ( config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Sports" ) ;
}
2016-09-15 06:23:39 +00:00
recordPath = Path . Combine ( recordPath , _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ) ;
2015-08-20 23:55:23 +00:00
}
2015-07-20 18:32:55 +00:00
else
{
2016-05-04 20:50:47 +00:00
if ( config . EnableRecordingSubfolders )
{
recordPath = Path . Combine ( recordPath , "Other" ) ;
}
2016-09-15 06:23:39 +00:00
recordPath = Path . Combine ( recordPath , _fileSystem . GetValidFilename ( timer . Name ) . Trim ( ) ) ;
2015-07-20 18:32:55 +00:00
}
2016-09-15 06:23:39 +00:00
var recordingFileName = _fileSystem . GetValidFilename ( RecordingHelper . GetRecordingName ( timer ) ) . Trim ( ) + ".ts" ;
2015-08-21 02:36:30 +00:00
2016-05-04 20:50:47 +00:00
return Path . Combine ( recordPath , recordingFileName ) ;
}
2015-07-20 18:32:55 +00:00
2016-09-26 18:59:18 +00:00
private async Task RecordStream ( TimerInfo timer , DateTime recordingEndDate ,
ActiveRecordingInfo activeRecordingInfo , CancellationToken cancellationToken )
2016-05-04 20:50:47 +00:00
{
if ( timer = = null )
{
throw new ArgumentNullException ( "timer" ) ;
}
2015-07-20 18:32:55 +00:00
2016-09-15 06:23:39 +00:00
ProgramInfo programInfo = null ;
2016-05-04 20:50:47 +00:00
2016-09-15 06:23:39 +00:00
if ( ! string . IsNullOrWhiteSpace ( timer . ProgramId ) )
2016-05-04 20:50:47 +00:00
{
2016-09-15 06:23:39 +00:00
programInfo = GetProgramInfoFromCache ( timer . ChannelId , timer . ProgramId ) ;
2016-05-04 20:50:47 +00:00
}
2016-09-15 06:23:39 +00:00
if ( programInfo = = null )
2016-05-04 20:50:47 +00:00
{
_logger . Info ( "Unable to find program with Id {0}. Will search using start date" , timer . ProgramId ) ;
2016-09-15 06:23:39 +00:00
programInfo = GetProgramInfoFromCache ( timer . ChannelId , timer . StartDate ) ;
2015-07-20 18:32:55 +00:00
}
2016-09-15 06:23:39 +00:00
if ( programInfo ! = null )
2016-05-04 20:50:47 +00:00
{
2016-09-15 06:23:39 +00:00
RecordingHelper . CopyProgramInfoToTimerInfo ( programInfo , timer ) ;
2016-10-09 07:18:43 +00:00
activeRecordingInfo . Program = programInfo ;
2016-05-04 20:50:47 +00:00
}
2016-09-15 23:19:27 +00:00
string seriesPath = null ;
var recordPath = GetRecordingPath ( timer , out seriesPath ) ;
2016-05-04 20:50:47 +00:00
var recordingStatus = RecordingStatus . New ;
2016-09-25 18:39:13 +00:00
2016-09-29 12:55:49 +00:00
string liveStreamId = null ;
2016-05-04 20:50:47 +00:00
2016-10-11 06:46:59 +00:00
OnRecordingStatusChanged ( ) ;
2015-07-20 18:32:55 +00:00
try
{
2016-09-30 06:50:06 +00:00
var allMediaSources = await GetChannelStreamMediaSources ( timer . ChannelId , CancellationToken . None ) . ConfigureAwait ( false ) ;
2016-09-25 18:39:13 +00:00
2016-09-29 12:55:49 +00:00
var liveStreamInfo = await GetChannelStreamInternal ( timer . ChannelId , allMediaSources [ 0 ] . Id , CancellationToken . None )
2016-09-26 18:59:18 +00:00
. ConfigureAwait ( false ) ;
2016-09-29 12:55:49 +00:00
var mediaStreamInfo = liveStreamInfo . Item2 ;
liveStreamId = mediaStreamInfo . Id ;
2015-09-26 02:31:13 +00:00
2016-06-27 22:53:42 +00:00
// HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
//await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
2016-02-12 07:01:38 +00:00
2016-06-27 22:53:42 +00:00
var recorder = await GetRecorder ( ) . ConfigureAwait ( false ) ;
2015-10-16 18:11:11 +00:00
2016-06-27 22:53:42 +00:00
recordPath = recorder . GetOutputPath ( mediaStreamInfo , recordPath ) ;
recordPath = EnsureFileUnique ( recordPath , timer . Id ) ;
2016-02-22 16:00:17 +00:00
2016-09-07 05:48:14 +00:00
_libraryManager . RegisterIgnoredPath ( recordPath ) ;
2016-06-27 22:53:42 +00:00
_libraryMonitor . ReportFileSystemChangeBeginning ( recordPath ) ;
_fileSystem . CreateDirectory ( Path . GetDirectoryName ( recordPath ) ) ;
activeRecordingInfo . Path = recordPath ;
2016-04-26 02:16:46 +00:00
2016-06-27 22:53:42 +00:00
var duration = recordingEndDate - DateTime . UtcNow ;
2015-10-16 18:11:11 +00:00
2016-09-26 18:59:18 +00:00
_logger . Info ( "Beginning recording. Will record for {0} minutes." ,
duration . TotalMinutes . ToString ( CultureInfo . InvariantCulture ) ) ;
2016-02-06 21:32:02 +00:00
2016-06-27 22:53:42 +00:00
_logger . Info ( "Writing file to path: " + recordPath ) ;
_logger . Info ( "Opening recording stream from tuner provider" ) ;
2016-02-06 21:32:02 +00:00
2016-06-27 22:53:42 +00:00
Action onStarted = ( ) = >
{
timer . Status = RecordingStatus . InProgress ;
_timerProvider . AddOrUpdate ( timer , false ) ;
2016-04-26 02:16:46 +00:00
2016-09-20 19:38:53 +00:00
SaveNfo ( timer , recordPath , seriesPath ) ;
2016-09-27 05:13:56 +00:00
EnforceKeepUpTo ( timer ) ;
2016-06-27 22:53:42 +00:00
} ;
2016-04-26 02:16:46 +00:00
2016-09-27 05:13:56 +00:00
await recorder . Record ( mediaStreamInfo , recordPath , duration , onStarted , cancellationToken )
2016-09-26 18:59:18 +00:00
. ConfigureAwait ( false ) ;
2016-06-27 22:53:42 +00:00
recordingStatus = RecordingStatus . Completed ;
_logger . Info ( "Recording completed: {0}" , recordPath ) ;
2015-07-20 18:32:55 +00:00
}
catch ( OperationCanceledException )
{
2016-03-01 04:24:42 +00:00
_logger . Info ( "Recording stopped: {0}" , recordPath ) ;
2016-05-04 20:50:47 +00:00
recordingStatus = RecordingStatus . Completed ;
2015-07-20 18:32:55 +00:00
}
2015-08-05 03:43:54 +00:00
catch ( Exception ex )
2015-07-20 18:32:55 +00:00
{
2016-03-01 04:24:42 +00:00
_logger . ErrorException ( "Error recording to {0}" , ex , recordPath ) ;
2016-05-04 20:50:47 +00:00
recordingStatus = RecordingStatus . Error ;
2015-07-20 18:32:55 +00:00
}
2016-09-25 18:39:13 +00:00
2016-09-29 12:55:49 +00:00
if ( ! string . IsNullOrWhiteSpace ( liveStreamId ) )
2015-09-04 01:34:57 +00:00
{
2016-09-25 18:39:13 +00:00
try
{
2016-09-29 12:55:49 +00:00
await CloseLiveStream ( liveStreamId , CancellationToken . None ) . ConfigureAwait ( false ) ;
2016-09-25 18:39:13 +00:00
}
catch ( Exception ex )
2016-06-27 22:53:42 +00:00
{
2016-09-25 18:39:13 +00:00
_logger . ErrorException ( "Error closing live stream" , ex ) ;
2016-06-27 22:53:42 +00:00
}
2016-09-25 18:39:13 +00:00
}
2016-06-27 22:53:42 +00:00
2016-09-25 18:39:13 +00:00
_libraryManager . UnRegisterIgnoredPath ( recordPath ) ;
_libraryMonitor . ReportFileSystemChangeComplete ( recordPath , true ) ;
2016-06-27 22:53:42 +00:00
2016-09-25 18:39:13 +00:00
ActiveRecordingInfo removed ;
_activeRecordings . TryRemove ( timer . Id , out removed ) ;
2015-07-20 18:32:55 +00:00
2016-09-26 18:59:18 +00:00
if ( recordingStatus ! = RecordingStatus . Completed & & DateTime . UtcNow < timer . EndDate )
2015-09-03 01:40:47 +00:00
{
2015-09-04 01:34:57 +00:00
const int retryIntervalSeconds = 60 ;
_logger . Info ( "Retrying recording in {0} seconds." , retryIntervalSeconds ) ;
2015-09-03 01:40:47 +00:00
2016-06-20 22:07:18 +00:00
timer . Status = RecordingStatus . New ;
timer . StartDate = DateTime . UtcNow . AddSeconds ( retryIntervalSeconds ) ;
_timerProvider . AddOrUpdate ( timer ) ;
2015-09-04 01:34:57 +00:00
}
2016-09-26 18:59:18 +00:00
else if ( File . Exists ( recordPath ) )
{
2016-09-27 05:13:56 +00:00
timer . RecordingPath = recordPath ;
2016-09-26 18:59:18 +00:00
timer . Status = RecordingStatus . Completed ;
_timerProvider . AddOrUpdate ( timer , false ) ;
OnSuccessfulRecording ( timer , recordPath ) ;
}
2015-09-04 01:34:57 +00:00
else
{
_timerProvider . Delete ( timer ) ;
2015-08-22 19:46:55 +00:00
}
2016-10-11 06:46:59 +00:00
OnRecordingStatusChanged ( ) ;
}
private void OnRecordingStatusChanged ( )
{
EventHelper . FireEventIfNotNull ( RecordingStatusChanged , this , new RecordingStatusChangedEventArgs
{
} , _logger ) ;
2015-08-22 19:46:55 +00:00
}
2016-09-27 05:13:56 +00:00
private async void EnforceKeepUpTo ( TimerInfo timer )
{
if ( string . IsNullOrWhiteSpace ( timer . SeriesTimerId ) )
{
return ;
}
var seriesTimerId = timer . SeriesTimerId ;
var seriesTimer = _seriesTimerProvider . GetAll ( ) . FirstOrDefault ( i = > string . Equals ( i . Id , seriesTimerId , StringComparison . OrdinalIgnoreCase ) ) ;
if ( seriesTimer = = null | | seriesTimer . KeepUpTo < = 1 )
{
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 )
. Where ( i = > File . Exists ( i . RecordingPath ) )
. Skip ( seriesTimer . KeepUpTo - 1 )
. ToList ( ) ;
await DeleteLibraryItemsForTimers ( timersToDelete ) . ConfigureAwait ( false ) ;
}
finally
{
_recordingDeleteSemaphore . Release ( ) ;
}
}
2016-09-29 12:55:49 +00:00
private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim ( 1 , 1 ) ;
2016-09-27 05:13:56 +00:00
private async Task DeleteLibraryItemsForTimers ( List < TimerInfo > timers )
{
foreach ( var timer in timers )
{
if ( _disposed )
{
return ;
}
try
{
await DeleteLibraryItemForTimer ( timer ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error deleting recording" , ex ) ;
}
}
}
private async Task DeleteLibraryItemForTimer ( TimerInfo timer )
{
var libraryItem = _libraryManager . FindByPath ( timer . RecordingPath , false ) ;
if ( libraryItem ! = null )
{
await _libraryManager . DeleteItem ( libraryItem , new DeleteOptions
{
DeleteFileLocation = true
} ) ;
}
else
{
try
{
File . Delete ( timer . RecordingPath ) ;
}
catch ( DirectoryNotFoundException )
{
}
catch ( FileNotFoundException )
{
2016-09-29 12:55:49 +00:00
2016-09-27 05:13:56 +00:00
}
}
_timerProvider . Delete ( timer ) ;
}
2016-03-01 04:24:42 +00:00
private string EnsureFileUnique ( string path , string timerId )
2016-02-26 04:09:42 +00:00
{
var originalPath = path ;
var index = 1 ;
2016-03-01 04:24:42 +00:00
while ( FileExists ( path , timerId ) )
2016-02-26 04:09:42 +00:00
{
var parent = Path . GetDirectoryName ( originalPath ) ;
var name = Path . GetFileNameWithoutExtension ( originalPath ) ;
name + = "-" + index . ToString ( CultureInfo . InvariantCulture ) ;
path = Path . ChangeExtension ( Path . Combine ( parent , name ) , Path . GetExtension ( originalPath ) ) ;
index + + ;
}
return path ;
}
2016-03-01 04:24:42 +00:00
private bool FileExists ( string path , string timerId )
{
if ( _fileSystem . FileExists ( path ) )
{
return true ;
}
2016-10-09 07:18:43 +00:00
var hasRecordingAtPath = _activeRecordings
. Values
. ToList ( )
. Any ( i = > string . Equals ( i . Path , path , StringComparison . OrdinalIgnoreCase ) & & ! string . Equals ( i . Timer . Id , timerId , StringComparison . OrdinalIgnoreCase ) ) ;
2016-03-01 04:24:42 +00:00
if ( hasRecordingAtPath )
{
return true ;
}
return false ;
}
2016-02-12 07:01:38 +00:00
private async Task < IRecorder > GetRecorder ( )
{
2016-04-27 21:26:28 +00:00
var config = GetConfiguration ( ) ;
if ( config . EnableRecordingEncoding )
2016-02-12 07:01:38 +00:00
{
2016-09-06 05:02:05 +00:00
var regInfo = await _liveTvManager . GetRegistrationInfo ( "embytvrecordingconversion" ) . ConfigureAwait ( false ) ;
2016-02-12 07:01:38 +00:00
if ( regInfo . IsValid )
{
2016-06-20 22:07:18 +00:00
return new EncodedRecorder ( _logger , _fileSystem , _mediaEncoder , _config . ApplicationPaths , _jsonSerializer , config , _httpClient ) ;
2016-02-12 07:01:38 +00:00
}
}
return new DirectRecorder ( _logger , _httpClient , _fileSystem ) ;
}
2016-09-20 19:38:53 +00:00
private async void OnSuccessfulRecording ( TimerInfo timer , string path )
2015-08-22 19:46:55 +00:00
{
2016-09-15 23:19:27 +00:00
if ( timer . IsProgramSeries & & GetConfiguration ( ) . EnableAutoOrganize )
2015-08-22 19:46:55 +00:00
{
2016-09-15 23:19:27 +00:00
try
2015-08-22 19:46:55 +00:00
{
2016-09-15 23:19:27 +00:00
// this is to account for the library monitor holding a lock for additional time after the change is complete.
// ideally this shouldn't be hard-coded
await Task . Delay ( 30000 ) . ConfigureAwait ( false ) ;
var organize = new EpisodeFileOrganizer ( _organizationService , _config , _fileSystem , _logger , _libraryManager , _libraryMonitor , _providerManager ) ;
2016-03-03 17:52:47 +00:00
2016-09-15 23:19:27 +00:00
var result = await organize . OrganizeEpisodeFile ( path , _config . GetAutoOrganizeOptions ( ) , false , CancellationToken . None ) . ConfigureAwait ( false ) ;
2015-08-22 19:46:55 +00:00
2016-09-15 23:19:27 +00:00
if ( result . Status = = FileSortingStatus . Success )
{
return ;
2015-08-22 19:46:55 +00:00
}
2016-09-15 23:19:27 +00:00
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error processing new recording" , ex ) ;
}
}
}
private void SaveNfo ( TimerInfo timer , string recordingPath , string seriesPath )
{
2016-09-20 19:38:53 +00:00
try
{
if ( timer . IsProgramSeries )
{
SaveSeriesNfo ( timer , recordingPath , seriesPath ) ;
}
2016-10-03 06:28:45 +00:00
else if ( ! timer . IsMovie | | timer . IsSports | | timer . IsNews )
2016-09-20 19:38:53 +00:00
{
SaveVideoNfo ( timer , recordingPath ) ;
}
}
catch ( Exception ex )
2016-09-15 23:19:27 +00:00
{
2016-09-20 19:38:53 +00:00
_logger . ErrorException ( "Error saving nfo" , ex ) ;
2016-09-15 23:19:27 +00:00
}
}
private void SaveSeriesNfo ( TimerInfo timer , string recordingPath , string seriesPath )
{
var nfoPath = Path . Combine ( seriesPath , "tvshow.nfo" ) ;
if ( File . Exists ( nfoPath ) )
{
return ;
}
using ( var stream = _fileSystem . GetFileStream ( nfoPath , FileMode . Create , FileAccess . Write , FileShare . Read ) )
{
var settings = new XmlWriterSettings
{
Indent = true ,
Encoding = Encoding . UTF8 ,
CloseOutput = false
} ;
using ( XmlWriter writer = XmlWriter . Create ( stream , settings ) )
{
writer . WriteStartDocument ( true ) ;
writer . WriteStartElement ( "tvshow" ) ;
if ( ! string . IsNullOrWhiteSpace ( timer . Name ) )
2015-08-22 19:46:55 +00:00
{
2016-09-15 23:19:27 +00:00
writer . WriteElementString ( "title" , timer . Name ) ;
2015-08-22 19:46:55 +00:00
}
2016-09-15 23:19:27 +00:00
writer . WriteEndElement ( ) ;
writer . WriteEndDocument ( ) ;
2015-08-22 19:46:55 +00:00
}
}
2015-07-20 18:32:55 +00:00
}
2016-09-20 19:38:53 +00:00
public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss" ;
private void SaveVideoNfo ( TimerInfo timer , string recordingPath )
{
var nfoPath = Path . ChangeExtension ( recordingPath , ".nfo" ) ;
if ( File . Exists ( nfoPath ) )
{
return ;
}
using ( var stream = _fileSystem . GetFileStream ( nfoPath , FileMode . Create , FileAccess . Write , FileShare . Read ) )
{
var settings = new XmlWriterSettings
{
Indent = true ,
Encoding = Encoding . UTF8 ,
CloseOutput = false
} ;
using ( XmlWriter writer = XmlWriter . Create ( stream , settings ) )
{
writer . WriteStartDocument ( true ) ;
writer . WriteStartElement ( "movie" ) ;
if ( ! string . IsNullOrWhiteSpace ( timer . Name ) )
{
writer . WriteElementString ( "title" , timer . Name ) ;
}
writer . WriteElementString ( "dateadded" , DateTime . UtcNow . ToLocalTime ( ) . ToString ( DateAddedFormat ) ) ;
if ( timer . ProductionYear . HasValue )
{
writer . WriteElementString ( "year" , timer . ProductionYear . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
}
if ( ! string . IsNullOrEmpty ( timer . OfficialRating ) )
{
writer . WriteElementString ( "mpaa" , timer . OfficialRating ) ;
}
var overview = ( timer . Overview ? ? string . Empty )
. StripHtml ( )
. Replace ( """ , "'" ) ;
writer . WriteElementString ( "plot" , overview ) ;
writer . WriteElementString ( "lockdata" , true . ToString ( ) . ToLower ( ) ) ;
if ( timer . CommunityRating . HasValue )
{
writer . WriteElementString ( "rating" , timer . CommunityRating . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
}
2016-09-21 17:07:18 +00:00
if ( timer . IsSports )
{
AddGenre ( timer . Genres , "Sports" ) ;
}
if ( timer . IsKids )
{
AddGenre ( timer . Genres , "Kids" ) ;
2016-10-07 15:08:13 +00:00
AddGenre ( timer . Genres , "Children" ) ;
2016-09-21 17:07:18 +00:00
}
2016-09-29 12:55:49 +00:00
if ( timer . IsNews )
{
AddGenre ( timer . Genres , "News" ) ;
}
2016-09-21 17:07:18 +00:00
2016-09-20 19:38:53 +00:00
foreach ( var genre in timer . Genres )
{
writer . WriteElementString ( "genre" , genre ) ;
}
if ( ! string . IsNullOrWhiteSpace ( timer . ShortOverview ) )
{
writer . WriteElementString ( "outline" , timer . ShortOverview ) ;
}
if ( ! string . IsNullOrWhiteSpace ( timer . HomePageUrl ) )
{
writer . WriteElementString ( "website" , timer . HomePageUrl ) ;
}
writer . WriteEndElement ( ) ;
writer . WriteEndDocument ( ) ;
}
}
}
2016-09-21 17:07:18 +00:00
private void AddGenre ( List < string > genres , string genre )
{
if ( ! genres . Contains ( genre , StringComparer . OrdinalIgnoreCase ) )
{
genres . Add ( genre ) ;
}
}
2015-07-20 18:32:55 +00:00
private ProgramInfo GetProgramInfoFromCache ( string channelId , string programId )
{
var epgData = GetEpgDataForChannel ( channelId ) ;
2015-08-18 04:22:45 +00:00
return epgData . FirstOrDefault ( p = > string . Equals ( p . Id , programId , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-20 18:32:55 +00:00
}
2016-02-06 21:32:02 +00:00
private ProgramInfo GetProgramInfoFromCache ( string channelId , DateTime startDateUtc )
{
var epgData = GetEpgDataForChannel ( channelId ) ;
var startDateTicks = startDateUtc . Ticks ;
// Find the first program that starts within 3 minutes
return epgData . FirstOrDefault ( p = > Math . Abs ( startDateTicks - p . StartDate . Ticks ) < = TimeSpan . FromMinutes ( 3 ) . Ticks ) ;
}
2015-07-20 18:32:55 +00:00
private LiveTvOptions GetConfiguration ( )
{
return _config . GetConfiguration < LiveTvOptions > ( "livetv" ) ;
}
2016-09-26 18:59:18 +00:00
private bool ShouldCancelTimerForSeriesTimer ( SeriesTimerInfo seriesTimer , TimerInfo timer )
{
2016-10-04 05:15:39 +00:00
if ( ! seriesTimer . RecordAnyTime )
{
if ( Math . Abs ( seriesTimer . StartDate . TimeOfDay . Ticks - timer . StartDate . TimeOfDay . Ticks ) > = TimeSpan . FromMinutes ( 5 ) . Ticks )
{
return true ;
}
if ( ! seriesTimer . Days . Contains ( timer . StartDate . ToLocalTime ( ) . DayOfWeek ) )
{
return true ;
}
}
if ( seriesTimer . RecordNewOnly & & timer . IsRepeat )
{
return true ;
}
if ( ! seriesTimer . RecordAnyChannel & & ! string . Equals ( timer . ChannelId , seriesTimer . ChannelId , StringComparison . OrdinalIgnoreCase ) )
{
return true ;
}
2016-09-26 18:59:18 +00:00
return seriesTimer . SkipEpisodesInLibrary & & IsProgramAlreadyInLibrary ( timer ) ;
}
2016-01-20 03:48:37 +00:00
private async Task UpdateTimersForSeriesTimer ( List < ProgramInfo > epgData , SeriesTimerInfo seriesTimer , bool deleteInvalidTimers )
2015-07-20 18:32:55 +00:00
{
2016-09-26 18:59:18 +00:00
var allTimers = GetTimersForSeries ( seriesTimer , epgData )
. ToList ( ) ;
2016-09-15 06:23:39 +00:00
2016-09-06 05:02:05 +00:00
var registration = await _liveTvManager . GetRegistrationInfo ( "seriesrecordings" ) . ConfigureAwait ( false ) ;
2015-08-21 19:25:35 +00:00
if ( registration . IsValid )
2015-07-20 18:32:55 +00:00
{
2016-09-26 18:59:18 +00:00
foreach ( var timer in allTimers )
2015-08-21 19:25:35 +00:00
{
2016-09-27 05:13:56 +00:00
var existingTimer = _timerProvider . GetTimer ( timer . Id ) ;
2016-09-26 18:59:18 +00:00
if ( existingTimer = = null )
{
if ( ShouldCancelTimerForSeriesTimer ( seriesTimer , timer ) )
{
timer . Status = RecordingStatus . Cancelled ;
}
_timerProvider . Add ( timer ) ;
}
else
{
// Only update if not currently active
ActiveRecordingInfo activeRecordingInfo ;
if ( ! _activeRecordings . TryGetValue ( timer . Id , out activeRecordingInfo ) )
{
2016-10-03 06:28:45 +00:00
UpdateExistingTimerWithNewMetadata ( existingTimer , timer ) ;
2016-09-26 18:59:18 +00:00
if ( ShouldCancelTimerForSeriesTimer ( seriesTimer , timer ) )
{
existingTimer . Status = RecordingStatus . Cancelled ;
}
2016-09-27 05:13:56 +00:00
existingTimer . SeriesTimerId = seriesTimer . Id ;
2016-09-26 18:59:18 +00:00
_timerProvider . Update ( existingTimer ) ;
}
}
2015-08-21 19:25:35 +00:00
}
2015-07-20 18:32:55 +00:00
}
2016-01-20 03:48:37 +00:00
if ( deleteInvalidTimers )
{
2016-09-26 18:59:18 +00:00
var allTimerIds = allTimers
2016-01-20 03:48:37 +00:00
. Select ( i = > i . Id )
. ToList ( ) ;
2016-09-26 18:59:18 +00:00
var deleteStatuses = new List < RecordingStatus >
{
RecordingStatus . New
} ;
2016-01-20 03:48:37 +00:00
var deletes = _timerProvider . GetAll ( )
. Where ( i = > string . Equals ( i . SeriesTimerId , seriesTimer . Id , StringComparison . OrdinalIgnoreCase ) )
2016-09-26 18:59:18 +00:00
. Where ( i = > ! allTimerIds . Contains ( i . Id , StringComparer . OrdinalIgnoreCase ) & & i . StartDate > DateTime . UtcNow )
. Where ( i = > deleteStatuses . Contains ( i . Status ) )
2016-01-20 03:48:37 +00:00
. ToList ( ) ;
foreach ( var timer in deletes )
{
2016-09-26 18:59:18 +00:00
CancelTimerInternal ( timer . Id , false ) ;
2016-01-20 03:48:37 +00:00
}
}
2015-07-20 18:32:55 +00:00
}
2016-05-04 20:50:47 +00:00
private IEnumerable < TimerInfo > GetTimersForSeries ( SeriesTimerInfo seriesTimer ,
2016-09-26 18:59:18 +00:00
IEnumerable < ProgramInfo > allPrograms )
2015-07-29 17:16:00 +00:00
{
2016-03-13 07:34:17 +00:00
if ( seriesTimer = = null )
{
throw new ArgumentNullException ( "seriesTimer" ) ;
}
if ( allPrograms = = null )
{
throw new ArgumentNullException ( "allPrograms" ) ;
}
2015-08-17 16:52:56 +00:00
// Exclude programs that have already ended
2016-09-14 16:21:33 +00:00
allPrograms = allPrograms . Where ( i = > i . EndDate > DateTime . UtcNow ) ;
2015-08-17 16:52:56 +00:00
2015-07-29 17:16:00 +00:00
allPrograms = GetProgramsForSeries ( seriesTimer , allPrograms ) ;
return allPrograms . Select ( i = > RecordingHelper . CreateTimer ( i , seriesTimer ) ) ;
}
2016-09-26 18:59:18 +00:00
private bool IsProgramAlreadyInLibrary ( TimerInfo program )
2016-05-04 20:50:47 +00:00
{
if ( ( program . EpisodeNumber . HasValue & & program . SeasonNumber . HasValue ) | | ! string . IsNullOrWhiteSpace ( program . EpisodeTitle ) )
{
var seriesIds = _libraryManager . GetItemIds ( new InternalItemsQuery
{
IncludeItemTypes = new [ ] { typeof ( Series ) . Name } ,
Name = program . Name
} ) . Select ( i = > i . ToString ( "N" ) ) . ToArray ( ) ;
if ( seriesIds . Length = = 0 )
{
return false ;
}
if ( program . EpisodeNumber . HasValue & & program . SeasonNumber . HasValue )
{
var result = _libraryManager . GetItemsResult ( new InternalItemsQuery
{
IncludeItemTypes = new [ ] { typeof ( Episode ) . Name } ,
ParentIndexNumber = program . SeasonNumber . Value ,
IndexNumber = program . EpisodeNumber . Value ,
2016-05-10 05:00:50 +00:00
AncestorIds = seriesIds ,
2016-10-17 16:35:29 +00:00
IsVirtualItem = false
2016-05-04 20:50:47 +00:00
} ) ;
if ( result . TotalRecordCount > 0 )
{
return true ;
}
}
}
return false ;
}
2015-07-29 17:16:00 +00:00
private IEnumerable < ProgramInfo > GetProgramsForSeries ( SeriesTimerInfo seriesTimer , IEnumerable < ProgramInfo > allPrograms )
{
2015-08-16 18:37:53 +00:00
if ( string . IsNullOrWhiteSpace ( seriesTimer . SeriesId ) )
{
_logger . Error ( "seriesTimer.SeriesId is null. Cannot find programs for series" ) ;
return new List < ProgramInfo > ( ) ;
}
2015-08-15 21:58:52 +00:00
return allPrograms . Where ( i = > string . Equals ( i . SeriesId , seriesTimer . SeriesId , StringComparison . OrdinalIgnoreCase ) ) ;
2015-07-29 17:16:00 +00:00
}
2015-08-02 23:47:31 +00:00
2015-07-20 18:32:55 +00:00
private string GetChannelEpgCachePath ( string channelId )
{
2015-10-24 15:33:22 +00:00
return Path . Combine ( _config . CommonApplicationPaths . CachePath , "embytvepg" , channelId + ".json" ) ;
2015-07-20 18:32:55 +00:00
}
private readonly object _epgLock = new object ( ) ;
private void SaveEpgDataForChannel ( string channelId , List < ProgramInfo > epgData )
{
var path = GetChannelEpgCachePath ( channelId ) ;
2015-09-21 16:26:05 +00:00
_fileSystem . CreateDirectory ( Path . GetDirectoryName ( path ) ) ;
2015-07-20 18:32:55 +00:00
lock ( _epgLock )
{
_jsonSerializer . SerializeToFile ( epgData , path ) ;
}
}
private List < ProgramInfo > GetEpgDataForChannel ( string channelId )
{
try
{
lock ( _epgLock )
{
return _jsonSerializer . DeserializeFromFile < List < ProgramInfo > > ( GetChannelEpgCachePath ( channelId ) ) ;
}
}
catch
{
return new List < ProgramInfo > ( ) ;
}
}
2015-08-15 21:58:52 +00:00
private List < ProgramInfo > GetEpgDataForChannels ( List < string > channelIds )
2015-07-20 18:32:55 +00:00
{
2015-08-15 21:58:52 +00:00
return channelIds . SelectMany ( GetEpgDataForChannel ) . ToList ( ) ;
2015-07-20 18:32:55 +00:00
}
2016-09-27 05:13:56 +00:00
private bool _disposed ;
2015-07-20 18:32:55 +00:00
public void Dispose ( )
{
2016-09-27 05:13:56 +00:00
_disposed = true ;
2015-07-20 18:32:55 +00:00
foreach ( var pair in _activeRecordings . ToList ( ) )
{
2016-03-01 04:24:42 +00:00
pair . Value . CancellationTokenSource . Cancel ( ) ;
2015-07-20 18:32:55 +00:00
}
}
2015-08-21 19:25:35 +00:00
2016-05-04 20:50:47 +00:00
public List < VirtualFolderInfo > GetRecordingFolders ( )
{
var list = new List < VirtualFolderInfo > ( ) ;
var defaultFolder = RecordingPath ;
var defaultName = "Recordings" ;
if ( Directory . Exists ( defaultFolder ) )
{
list . Add ( new VirtualFolderInfo
{
Locations = new List < string > { defaultFolder } ,
Name = defaultName
} ) ;
}
var customPath = GetConfiguration ( ) . MovieRecordingPath ;
if ( ( ! string . IsNullOrWhiteSpace ( customPath ) & & ! string . Equals ( customPath , defaultFolder , StringComparison . OrdinalIgnoreCase ) ) & & Directory . Exists ( customPath ) )
{
list . Add ( new VirtualFolderInfo
{
Locations = new List < string > { customPath } ,
Name = "Recorded Movies" ,
CollectionType = CollectionType . Movies
} ) ;
}
customPath = GetConfiguration ( ) . SeriesRecordingPath ;
if ( ( ! string . IsNullOrWhiteSpace ( customPath ) & & ! string . Equals ( customPath , defaultFolder , StringComparison . OrdinalIgnoreCase ) ) & & Directory . Exists ( customPath ) )
{
list . Add ( new VirtualFolderInfo
{
Locations = new List < string > { customPath } ,
Name = "Recorded Series" ,
CollectionType = CollectionType . TvShows
} ) ;
}
return list ;
}
2016-03-01 04:24:42 +00:00
class ActiveRecordingInfo
{
public string Path { get ; set ; }
2016-10-09 07:18:43 +00:00
public TimerInfo Timer { get ; set ; }
public ProgramInfo Program { get ; set ; }
2016-03-01 04:24:42 +00:00
public CancellationTokenSource CancellationTokenSource { get ; set ; }
}
2015-07-20 18:32:55 +00:00
}
2015-09-21 16:26:05 +00:00
}