EmbyTV.cs 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971
  1. using MediaBrowser.Common;
  2. using MediaBrowser.Common.Configuration;
  3. using MediaBrowser.Common.Net;
  4. using MediaBrowser.Common.Security;
  5. using MediaBrowser.Controller.Configuration;
  6. using MediaBrowser.Controller.Drawing;
  7. using MediaBrowser.Controller.FileOrganization;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Controller.LiveTv;
  10. using MediaBrowser.Controller.MediaEncoding;
  11. using MediaBrowser.Controller.Providers;
  12. using MediaBrowser.Model.Dto;
  13. using MediaBrowser.Model.Entities;
  14. using MediaBrowser.Model.Events;
  15. using MediaBrowser.Model.LiveTv;
  16. using MediaBrowser.Model.Logging;
  17. using MediaBrowser.Model.Serialization;
  18. using MediaBrowser.Server.Implementations.FileOrganization;
  19. using System;
  20. using System.Collections.Concurrent;
  21. using System.Collections.Generic;
  22. using System.Globalization;
  23. using System.IO;
  24. using System.Linq;
  25. using System.Text;
  26. using System.Threading;
  27. using System.Threading.Tasks;
  28. using System.Xml;
  29. using CommonIO;
  30. using MediaBrowser.Common.Extensions;
  31. using MediaBrowser.Controller;
  32. using MediaBrowser.Controller.Entities;
  33. using MediaBrowser.Controller.Entities.TV;
  34. using MediaBrowser.Model.Configuration;
  35. using MediaBrowser.Model.FileOrganization;
  36. using Microsoft.Win32;
  37. namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
  38. {
  39. public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
  40. {
  41. private readonly IServerApplicationHost _appHost;
  42. private readonly ILogger _logger;
  43. private readonly IHttpClient _httpClient;
  44. private readonly IServerConfigurationManager _config;
  45. private readonly IJsonSerializer _jsonSerializer;
  46. private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
  47. private readonly TimerManager _timerProvider;
  48. private readonly LiveTvManager _liveTvManager;
  49. private readonly IFileSystem _fileSystem;
  50. private readonly ILibraryMonitor _libraryMonitor;
  51. private readonly ILibraryManager _libraryManager;
  52. private readonly IProviderManager _providerManager;
  53. private readonly IFileOrganizationService _organizationService;
  54. private readonly IMediaEncoder _mediaEncoder;
  55. public static EmbyTV Current;
  56. public event EventHandler DataSourceChanged { add { } remove { } }
  57. public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged { add { } remove { } }
  58. private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
  59. new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
  60. 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)
  61. {
  62. Current = this;
  63. _appHost = appHost;
  64. _logger = logger;
  65. _httpClient = httpClient;
  66. _config = config;
  67. _fileSystem = fileSystem;
  68. _libraryManager = libraryManager;
  69. _libraryMonitor = libraryMonitor;
  70. _providerManager = providerManager;
  71. _organizationService = organizationService;
  72. _mediaEncoder = mediaEncoder;
  73. _liveTvManager = (LiveTvManager)liveTvManager;
  74. _jsonSerializer = jsonSerializer;
  75. _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
  76. _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
  77. _timerProvider.TimerFired += _timerProvider_TimerFired;
  78. _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
  79. }
  80. private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
  81. {
  82. if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
  83. {
  84. OnRecordingFoldersChanged();
  85. }
  86. }
  87. public void Start()
  88. {
  89. _timerProvider.RestartTimers();
  90. SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
  91. CreateRecordingFolders();
  92. }
  93. private void OnRecordingFoldersChanged()
  94. {
  95. CreateRecordingFolders();
  96. }
  97. internal void CreateRecordingFolders()
  98. {
  99. try
  100. {
  101. CreateRecordingFoldersInternal();
  102. }
  103. catch (Exception ex)
  104. {
  105. _logger.ErrorException("Error creating recording folders", ex);
  106. }
  107. }
  108. internal void CreateRecordingFoldersInternal()
  109. {
  110. var recordingFolders = GetRecordingFolders();
  111. var virtualFolders = _libraryManager.GetVirtualFolders()
  112. .ToList();
  113. var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
  114. var pathsAdded = new List<string>();
  115. foreach (var recordingFolder in recordingFolders)
  116. {
  117. var pathsToCreate = recordingFolder.Locations
  118. .Where(i => !allExistingPaths.Contains(i, StringComparer.OrdinalIgnoreCase))
  119. .ToList();
  120. if (pathsToCreate.Count == 0)
  121. {
  122. continue;
  123. }
  124. var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
  125. var libraryOptions = new LibraryOptions
  126. {
  127. PathInfos = mediaPathInfos
  128. };
  129. try
  130. {
  131. _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true);
  132. }
  133. catch (Exception ex)
  134. {
  135. _logger.ErrorException("Error creating virtual folder", ex);
  136. }
  137. pathsAdded.AddRange(pathsToCreate);
  138. }
  139. var config = GetConfiguration();
  140. var pathsToRemove = config.MediaLocationsCreated
  141. .Except(recordingFolders.SelectMany(i => i.Locations))
  142. .ToList();
  143. if (pathsAdded.Count > 0 || pathsToRemove.Count > 0)
  144. {
  145. pathsAdded.InsertRange(0, config.MediaLocationsCreated);
  146. config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  147. _config.SaveConfiguration("livetv", config);
  148. }
  149. foreach (var path in pathsToRemove)
  150. {
  151. RemovePathFromLibrary(path);
  152. }
  153. }
  154. private void RemovePathFromLibrary(string path)
  155. {
  156. _logger.Debug("Removing path from library: {0}", path);
  157. var requiresRefresh = false;
  158. var virtualFolders = _libraryManager.GetVirtualFolders()
  159. .ToList();
  160. foreach (var virtualFolder in virtualFolders)
  161. {
  162. if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
  163. {
  164. continue;
  165. }
  166. if (virtualFolder.Locations.Count == 1)
  167. {
  168. // remove entire virtual folder
  169. try
  170. {
  171. _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true);
  172. }
  173. catch (Exception ex)
  174. {
  175. _logger.ErrorException("Error removing virtual folder", ex);
  176. }
  177. }
  178. else
  179. {
  180. try
  181. {
  182. _libraryManager.RemoveMediaPath(virtualFolder.Name, path);
  183. requiresRefresh = true;
  184. }
  185. catch (Exception ex)
  186. {
  187. _logger.ErrorException("Error removing media path", ex);
  188. }
  189. }
  190. }
  191. if (requiresRefresh)
  192. {
  193. _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None);
  194. }
  195. }
  196. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  197. {
  198. _logger.Info("Power mode changed to {0}", e.Mode);
  199. if (e.Mode == PowerModes.Resume)
  200. {
  201. _timerProvider.RestartTimers();
  202. }
  203. }
  204. public string Name
  205. {
  206. get { return "Emby"; }
  207. }
  208. public string DataPath
  209. {
  210. get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
  211. }
  212. private string DefaultRecordingPath
  213. {
  214. get
  215. {
  216. return Path.Combine(DataPath, "recordings");
  217. }
  218. }
  219. private string RecordingPath
  220. {
  221. get
  222. {
  223. var path = GetConfiguration().RecordingPath;
  224. return string.IsNullOrWhiteSpace(path)
  225. ? DefaultRecordingPath
  226. : path;
  227. }
  228. }
  229. public string HomePageUrl
  230. {
  231. get { return "http://emby.media"; }
  232. }
  233. public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
  234. {
  235. var status = new LiveTvServiceStatusInfo();
  236. var list = new List<LiveTvTunerInfo>();
  237. foreach (var hostInstance in _liveTvManager.TunerHosts)
  238. {
  239. try
  240. {
  241. var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
  242. list.AddRange(tuners);
  243. }
  244. catch (Exception ex)
  245. {
  246. _logger.ErrorException("Error getting tuners", ex);
  247. }
  248. }
  249. status.Tuners = list;
  250. status.Status = LiveTvServiceStatus.Ok;
  251. status.Version = _appHost.ApplicationVersion.ToString();
  252. status.IsVisible = false;
  253. return status;
  254. }
  255. public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
  256. {
  257. var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
  258. List<ChannelInfo> channels = null;
  259. foreach (var timer in seriesTimers)
  260. {
  261. List<ProgramInfo> epgData;
  262. if (timer.RecordAnyChannel)
  263. {
  264. if (channels == null)
  265. {
  266. channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList();
  267. }
  268. var channelIds = channels.Select(i => i.Id).ToList();
  269. epgData = GetEpgDataForChannels(channelIds);
  270. }
  271. else
  272. {
  273. epgData = GetEpgDataForChannel(timer.ChannelId);
  274. }
  275. await UpdateTimersForSeriesTimer(epgData, timer, true).ConfigureAwait(false);
  276. }
  277. var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
  278. foreach (var timer in timers.ToList())
  279. {
  280. if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
  281. {
  282. OnTimerOutOfDate(timer);
  283. }
  284. }
  285. }
  286. private void OnTimerOutOfDate(TimerInfo timer)
  287. {
  288. _timerProvider.Delete(timer);
  289. }
  290. private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
  291. {
  292. var list = new List<ChannelInfo>();
  293. foreach (var hostInstance in _liveTvManager.TunerHosts)
  294. {
  295. try
  296. {
  297. var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
  298. list.AddRange(channels);
  299. }
  300. catch (Exception ex)
  301. {
  302. _logger.ErrorException("Error getting channels", ex);
  303. }
  304. }
  305. foreach (var provider in GetListingProviders())
  306. {
  307. var enabledChannels = list
  308. .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
  309. .ToList();
  310. if (enabledChannels.Count > 0)
  311. {
  312. try
  313. {
  314. await provider.Item1.AddMetadata(provider.Item2, enabledChannels, cancellationToken).ConfigureAwait(false);
  315. }
  316. catch (NotSupportedException)
  317. {
  318. }
  319. catch (Exception ex)
  320. {
  321. _logger.ErrorException("Error adding metadata", ex);
  322. }
  323. }
  324. }
  325. return list;
  326. }
  327. public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
  328. {
  329. var list = new List<ChannelInfo>();
  330. foreach (var hostInstance in _liveTvManager.TunerHosts)
  331. {
  332. try
  333. {
  334. var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
  335. list.AddRange(channels);
  336. }
  337. catch (Exception ex)
  338. {
  339. _logger.ErrorException("Error getting channels", ex);
  340. }
  341. }
  342. return list
  343. .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId))
  344. .ToList();
  345. }
  346. public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
  347. {
  348. return GetChannelsAsync(false, cancellationToken);
  349. }
  350. public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
  351. {
  352. var timers = _timerProvider
  353. .GetAll()
  354. .Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
  355. .ToList();
  356. foreach (var timer in timers)
  357. {
  358. CancelTimerInternal(timer.Id, true);
  359. }
  360. var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  361. if (remove != null)
  362. {
  363. _seriesTimerProvider.Delete(remove);
  364. }
  365. return Task.FromResult(true);
  366. }
  367. private void CancelTimerInternal(string timerId, bool isSeriesCancelled)
  368. {
  369. var timer = _timerProvider.GetTimer(timerId);
  370. if (timer != null)
  371. {
  372. if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
  373. {
  374. _timerProvider.Delete(timer);
  375. }
  376. else
  377. {
  378. timer.Status = RecordingStatus.Cancelled;
  379. _timerProvider.AddOrUpdate(timer, false);
  380. }
  381. }
  382. ActiveRecordingInfo activeRecordingInfo;
  383. if (_activeRecordings.TryGetValue(timerId, out activeRecordingInfo))
  384. {
  385. activeRecordingInfo.CancellationTokenSource.Cancel();
  386. }
  387. }
  388. public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
  389. {
  390. CancelTimerInternal(timerId, false);
  391. return Task.FromResult(true);
  392. }
  393. public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
  394. {
  395. return Task.FromResult(true);
  396. }
  397. public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  398. {
  399. throw new NotImplementedException();
  400. }
  401. public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  402. {
  403. throw new NotImplementedException();
  404. }
  405. public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
  406. {
  407. var existingTimer = _timerProvider.GetAll()
  408. .FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase));
  409. if (existingTimer != null)
  410. {
  411. if (existingTimer.Status == RecordingStatus.Cancelled ||
  412. existingTimer.Status == RecordingStatus.Completed)
  413. {
  414. existingTimer.Status = RecordingStatus.New;
  415. _timerProvider.Update(existingTimer);
  416. return Task.FromResult(existingTimer.Id);
  417. }
  418. else
  419. {
  420. throw new ArgumentException("A scheduled recording already exists for this program.");
  421. }
  422. }
  423. timer.Id = Guid.NewGuid().ToString("N");
  424. ProgramInfo programInfo = null;
  425. if (!string.IsNullOrWhiteSpace(timer.ProgramId))
  426. {
  427. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  428. }
  429. if (programInfo == null)
  430. {
  431. _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
  432. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
  433. }
  434. if (programInfo != null)
  435. {
  436. RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
  437. }
  438. _timerProvider.Add(timer);
  439. return Task.FromResult(timer.Id);
  440. }
  441. public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
  442. {
  443. info.Id = Guid.NewGuid().ToString("N");
  444. List<ProgramInfo> epgData;
  445. if (info.RecordAnyChannel)
  446. {
  447. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  448. var channelIds = channels.Select(i => i.Id).ToList();
  449. epgData = GetEpgDataForChannels(channelIds);
  450. }
  451. else
  452. {
  453. epgData = GetEpgDataForChannel(info.ChannelId);
  454. }
  455. // populate info.seriesID
  456. var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
  457. if (program != null)
  458. {
  459. info.SeriesId = program.SeriesId;
  460. }
  461. else
  462. {
  463. throw new InvalidOperationException("SeriesId for program not found");
  464. }
  465. _seriesTimerProvider.Add(info);
  466. await UpdateTimersForSeriesTimer(epgData, info, false).ConfigureAwait(false);
  467. return info.Id;
  468. }
  469. public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  470. {
  471. var instance = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
  472. if (instance != null)
  473. {
  474. instance.ChannelId = info.ChannelId;
  475. instance.Days = info.Days;
  476. instance.EndDate = info.EndDate;
  477. instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
  478. instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
  479. instance.PostPaddingSeconds = info.PostPaddingSeconds;
  480. instance.PrePaddingSeconds = info.PrePaddingSeconds;
  481. instance.Priority = info.Priority;
  482. instance.RecordAnyChannel = info.RecordAnyChannel;
  483. instance.RecordAnyTime = info.RecordAnyTime;
  484. instance.RecordNewOnly = info.RecordNewOnly;
  485. instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
  486. instance.KeepUpTo = info.KeepUpTo;
  487. instance.KeepUntil = info.KeepUntil;
  488. instance.StartDate = info.StartDate;
  489. _seriesTimerProvider.Update(instance);
  490. List<ProgramInfo> epgData;
  491. if (instance.RecordAnyChannel)
  492. {
  493. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  494. var channelIds = channels.Select(i => i.Id).ToList();
  495. epgData = GetEpgDataForChannels(channelIds);
  496. }
  497. else
  498. {
  499. epgData = GetEpgDataForChannel(instance.ChannelId);
  500. }
  501. await UpdateTimersForSeriesTimer(epgData, instance, true).ConfigureAwait(false);
  502. }
  503. }
  504. public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
  505. {
  506. var existingTimer = _timerProvider.GetTimer(updatedTimer.Id);
  507. if (existingTimer == null)
  508. {
  509. throw new ResourceNotFoundException();
  510. }
  511. // Only update if not currently active
  512. ActiveRecordingInfo activeRecordingInfo;
  513. if (!_activeRecordings.TryGetValue(updatedTimer.Id, out activeRecordingInfo))
  514. {
  515. existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
  516. existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
  517. existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
  518. existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
  519. }
  520. return Task.FromResult(true);
  521. }
  522. private void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
  523. {
  524. // Update the program info but retain the status
  525. existingTimer.ChannelId = updatedTimer.ChannelId;
  526. existingTimer.CommunityRating = updatedTimer.CommunityRating;
  527. existingTimer.EndDate = updatedTimer.EndDate;
  528. existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
  529. existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
  530. existingTimer.Genres = updatedTimer.Genres;
  531. existingTimer.HomePageUrl = updatedTimer.HomePageUrl;
  532. existingTimer.IsKids = updatedTimer.IsKids;
  533. existingTimer.IsNews = updatedTimer.IsNews;
  534. existingTimer.IsMovie = updatedTimer.IsMovie;
  535. existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
  536. existingTimer.IsRepeat = updatedTimer.IsRepeat;
  537. existingTimer.IsSports = updatedTimer.IsSports;
  538. existingTimer.Name = updatedTimer.Name;
  539. existingTimer.OfficialRating = updatedTimer.OfficialRating;
  540. existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
  541. existingTimer.Overview = updatedTimer.Overview;
  542. existingTimer.ProductionYear = updatedTimer.ProductionYear;
  543. existingTimer.ProgramId = updatedTimer.ProgramId;
  544. existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
  545. existingTimer.ShortOverview = updatedTimer.ShortOverview;
  546. existingTimer.StartDate = updatedTimer.StartDate;
  547. }
  548. public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
  549. {
  550. throw new NotImplementedException();
  551. }
  552. public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
  553. {
  554. throw new NotImplementedException();
  555. }
  556. public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
  557. {
  558. throw new NotImplementedException();
  559. }
  560. public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
  561. {
  562. return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList();
  563. }
  564. public string GetActiveRecordingPath(string id)
  565. {
  566. ActiveRecordingInfo info;
  567. if (_activeRecordings.TryGetValue(id, out info))
  568. {
  569. return info.Path;
  570. }
  571. return null;
  572. }
  573. private RecordingInfo GetRecordingInfo(ActiveRecordingInfo info)
  574. {
  575. var timer = info.Timer;
  576. var program = info.Program;
  577. var result = new RecordingInfo
  578. {
  579. ChannelId = timer.ChannelId,
  580. CommunityRating = timer.CommunityRating,
  581. DateLastUpdated = DateTime.UtcNow,
  582. EndDate = timer.EndDate,
  583. EpisodeTitle = timer.EpisodeTitle,
  584. Genres = timer.Genres,
  585. Id = "recording" + timer.Id,
  586. IsKids = timer.IsKids,
  587. IsMovie = timer.IsMovie,
  588. IsNews = timer.IsNews,
  589. IsRepeat = timer.IsRepeat,
  590. IsSeries = timer.IsProgramSeries,
  591. IsSports = timer.IsSports,
  592. Name = timer.Name,
  593. OfficialRating = timer.OfficialRating,
  594. OriginalAirDate = timer.OriginalAirDate,
  595. Overview = timer.Overview,
  596. ProgramId = timer.ProgramId,
  597. SeriesTimerId = timer.SeriesTimerId,
  598. StartDate = timer.StartDate,
  599. Status = RecordingStatus.InProgress,
  600. TimerId = timer.Id
  601. };
  602. if (program != null)
  603. {
  604. result.Audio = program.Audio;
  605. result.ImagePath = program.ImagePath;
  606. result.ImageUrl = program.ImageUrl;
  607. result.IsHD = program.IsHD;
  608. result.IsLive = program.IsLive;
  609. result.IsPremiere = program.IsPremiere;
  610. result.ShowId = program.ShowId;
  611. }
  612. return result;
  613. }
  614. public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
  615. {
  616. var excludeStatues = new List<RecordingStatus>
  617. {
  618. RecordingStatus.Completed
  619. };
  620. var timers = _timerProvider.GetAll()
  621. .Where(i => !excludeStatues.Contains(i.Status));
  622. return Task.FromResult(timers);
  623. }
  624. public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
  625. {
  626. var config = GetConfiguration();
  627. var defaults = new SeriesTimerInfo()
  628. {
  629. PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
  630. PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
  631. RecordAnyChannel = true,
  632. RecordAnyTime = true,
  633. RecordNewOnly = true,
  634. Days = new List<DayOfWeek>
  635. {
  636. DayOfWeek.Sunday,
  637. DayOfWeek.Monday,
  638. DayOfWeek.Tuesday,
  639. DayOfWeek.Wednesday,
  640. DayOfWeek.Thursday,
  641. DayOfWeek.Friday,
  642. DayOfWeek.Saturday
  643. }
  644. };
  645. if (program != null)
  646. {
  647. defaults.SeriesId = program.SeriesId;
  648. defaults.ProgramId = program.Id;
  649. }
  650. defaults.SkipEpisodesInLibrary = true;
  651. defaults.KeepUntil = KeepUntil.UntilDeleted;
  652. return Task.FromResult(defaults);
  653. }
  654. public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
  655. {
  656. return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
  657. }
  658. public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  659. {
  660. try
  661. {
  662. return await GetProgramsAsyncInternal(channelId, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false);
  663. }
  664. catch (OperationCanceledException)
  665. {
  666. throw;
  667. }
  668. catch (Exception ex)
  669. {
  670. _logger.ErrorException("Error getting programs", ex);
  671. return GetEpgDataForChannel(channelId).Where(i => i.StartDate <= endDateUtc && i.EndDate >= startDateUtc);
  672. }
  673. }
  674. private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
  675. {
  676. if (info.EnableAllTuners)
  677. {
  678. return true;
  679. }
  680. if (string.IsNullOrWhiteSpace(tunerHostId))
  681. {
  682. throw new ArgumentNullException("tunerHostId");
  683. }
  684. return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
  685. }
  686. private async Task<IEnumerable<ProgramInfo>> GetProgramsAsyncInternal(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  687. {
  688. var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
  689. var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  690. foreach (var provider in GetListingProviders())
  691. {
  692. if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
  693. {
  694. _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);
  695. continue;
  696. }
  697. _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
  698. var channelMappings = GetChannelMappings(provider.Item2);
  699. var channelNumber = channel.Number;
  700. string mappedChannelNumber;
  701. if (channelMappings.TryGetValue(channelNumber, out mappedChannelNumber))
  702. {
  703. _logger.Debug("Found mapped channel on provider {0}. Tuner channel number: {1}, Mapped channel number: {2}", provider.Item1.Name, channelNumber, mappedChannelNumber);
  704. channelNumber = mappedChannelNumber;
  705. }
  706. var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channelNumber, channel.Name, startDateUtc, endDateUtc, cancellationToken)
  707. .ConfigureAwait(false);
  708. var list = programs.ToList();
  709. // Replace the value that came from the provider with a normalized value
  710. foreach (var program in list)
  711. {
  712. program.ChannelId = channelId;
  713. }
  714. if (list.Count > 0)
  715. {
  716. SaveEpgDataForChannel(channelId, list);
  717. return list;
  718. }
  719. }
  720. return new List<ProgramInfo>();
  721. }
  722. private Dictionary<string, string> GetChannelMappings(ListingsProviderInfo info)
  723. {
  724. var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  725. foreach (var mapping in info.ChannelMappings)
  726. {
  727. dict[mapping.Name] = mapping.Value;
  728. }
  729. return dict;
  730. }
  731. private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
  732. {
  733. return GetConfiguration().ListingProviders
  734. .Select(i =>
  735. {
  736. var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
  737. return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
  738. })
  739. .Where(i => i != null)
  740. .ToList();
  741. }
  742. public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
  743. {
  744. throw new NotImplementedException();
  745. }
  746. private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
  747. private readonly Dictionary<string, LiveStream> _liveStreams = new Dictionary<string, LiveStream>();
  748. public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
  749. {
  750. var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false);
  751. return result.Item1;
  752. }
  753. public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken)
  754. {
  755. var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
  756. return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider);
  757. }
  758. private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
  759. {
  760. var json = _jsonSerializer.SerializeToString(mediaSource);
  761. mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
  762. mediaSource.Id = Guid.NewGuid().ToString("N") + "_" + mediaSource.Id;
  763. //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
  764. //{
  765. // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
  766. // ticks = Math.Max(0, ticks);
  767. // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
  768. //}
  769. return mediaSource;
  770. }
  771. public async Task<LiveStream> GetLiveStream(string uniqueId)
  772. {
  773. await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
  774. try
  775. {
  776. return _liveStreams.Values
  777. .FirstOrDefault(i => string.Equals(i.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
  778. }
  779. finally
  780. {
  781. _liveStreamsSemaphore.Release();
  782. }
  783. }
  784. private async Task<Tuple<LiveStream, MediaSourceInfo, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
  785. {
  786. _logger.Info("Streaming Channel " + channelId);
  787. await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  788. var result = _liveStreams.Values.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase));
  789. if (result != null && result.EnableStreamSharing)
  790. {
  791. result.ConsumerCount++;
  792. _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
  793. var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
  794. _liveStreamsSemaphore.Release();
  795. return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, result.TunerHost);
  796. }
  797. try
  798. {
  799. foreach (var hostInstance in _liveTvManager.TunerHosts)
  800. {
  801. try
  802. {
  803. result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  804. var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
  805. _liveStreams[openedMediaSource.Id] = result;
  806. result.ConsumerCount++;
  807. result.TunerHost = hostInstance;
  808. result.OriginalStreamId = streamId;
  809. _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}",
  810. streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId);
  811. return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, hostInstance);
  812. }
  813. catch (FileNotFoundException)
  814. {
  815. }
  816. catch (OperationCanceledException)
  817. {
  818. }
  819. }
  820. }
  821. finally
  822. {
  823. _liveStreamsSemaphore.Release();
  824. }
  825. throw new ApplicationException("Tuner not found.");
  826. }
  827. public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
  828. {
  829. foreach (var hostInstance in _liveTvManager.TunerHosts)
  830. {
  831. try
  832. {
  833. var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
  834. if (sources.Count > 0)
  835. {
  836. return sources;
  837. }
  838. }
  839. catch (NotImplementedException)
  840. {
  841. }
  842. }
  843. throw new NotImplementedException();
  844. }
  845. public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
  846. {
  847. ActiveRecordingInfo info;
  848. recordingId = recordingId.Replace("recording", string.Empty);
  849. if (_activeRecordings.TryGetValue(recordingId, out info))
  850. {
  851. return Task.FromResult(new List<MediaSourceInfo>
  852. {
  853. new MediaSourceInfo
  854. {
  855. Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveRecordings/" + recordingId + "/stream",
  856. Id = recordingId,
  857. SupportsDirectPlay = false,
  858. SupportsDirectStream = true,
  859. SupportsTranscoding = true,
  860. IsInfiniteStream = true,
  861. RequiresOpening = false,
  862. RequiresClosing = false,
  863. Protocol = Model.MediaInfo.MediaProtocol.Http,
  864. BufferMs = 0
  865. }
  866. });
  867. }
  868. throw new FileNotFoundException();
  869. }
  870. public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
  871. {
  872. // Ignore the consumer id
  873. //id = id.Substring(id.IndexOf('_') + 1);
  874. await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  875. try
  876. {
  877. LiveStream stream;
  878. if (_liveStreams.TryGetValue(id, out stream))
  879. {
  880. stream.ConsumerCount--;
  881. _logger.Info("Live stream {0} consumer count is now {1}", id, stream.ConsumerCount);
  882. if (stream.ConsumerCount <= 0)
  883. {
  884. _liveStreams.Remove(id);
  885. _logger.Info("Closing live stream {0}", id);
  886. await stream.Close().ConfigureAwait(false);
  887. _logger.Info("Live stream {0} closed successfully", id);
  888. }
  889. }
  890. else
  891. {
  892. _logger.Warn("Live stream not found: {0}, unable to close", id);
  893. }
  894. }
  895. catch (OperationCanceledException)
  896. {
  897. }
  898. catch (Exception ex)
  899. {
  900. _logger.ErrorException("Error closing live stream", ex);
  901. }
  902. finally
  903. {
  904. _liveStreamsSemaphore.Release();
  905. }
  906. }
  907. public Task RecordLiveStream(string id, CancellationToken cancellationToken)
  908. {
  909. return Task.FromResult(0);
  910. }
  911. public Task ResetTuner(string id, CancellationToken cancellationToken)
  912. {
  913. return Task.FromResult(0);
  914. }
  915. async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
  916. {
  917. var timer = e.Argument;
  918. _logger.Info("Recording timer fired.");
  919. try
  920. {
  921. var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
  922. if (recordingEndDate <= DateTime.UtcNow)
  923. {
  924. _logger.Warn("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
  925. OnTimerOutOfDate(timer);
  926. return;
  927. }
  928. var activeRecordingInfo = new ActiveRecordingInfo
  929. {
  930. CancellationTokenSource = new CancellationTokenSource(),
  931. Timer = timer
  932. };
  933. if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
  934. {
  935. await RecordStream(timer, recordingEndDate, activeRecordingInfo, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
  936. }
  937. else
  938. {
  939. _logger.Info("Skipping RecordStream because it's already in progress.");
  940. }
  941. }
  942. catch (OperationCanceledException)
  943. {
  944. }
  945. catch (Exception ex)
  946. {
  947. _logger.ErrorException("Error recording stream", ex);
  948. }
  949. }
  950. private string GetRecordingPath(TimerInfo timer, out string seriesPath)
  951. {
  952. var recordPath = RecordingPath;
  953. var config = GetConfiguration();
  954. seriesPath = null;
  955. if (timer.IsProgramSeries)
  956. {
  957. var customRecordingPath = config.SeriesRecordingPath;
  958. var allowSubfolder = true;
  959. if (!string.IsNullOrWhiteSpace(customRecordingPath))
  960. {
  961. allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
  962. recordPath = customRecordingPath;
  963. }
  964. if (allowSubfolder && config.EnableRecordingSubfolders)
  965. {
  966. recordPath = Path.Combine(recordPath, "Series");
  967. }
  968. var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
  969. // Can't use the year here in the folder name because it is the year of the episode, not the series.
  970. recordPath = Path.Combine(recordPath, folderName);
  971. seriesPath = recordPath;
  972. if (timer.SeasonNumber.HasValue)
  973. {
  974. folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
  975. recordPath = Path.Combine(recordPath, folderName);
  976. }
  977. }
  978. else if (timer.IsMovie)
  979. {
  980. var customRecordingPath = config.MovieRecordingPath;
  981. var allowSubfolder = true;
  982. if (!string.IsNullOrWhiteSpace(customRecordingPath))
  983. {
  984. allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
  985. recordPath = customRecordingPath;
  986. }
  987. if (allowSubfolder && config.EnableRecordingSubfolders)
  988. {
  989. recordPath = Path.Combine(recordPath, "Movies");
  990. }
  991. var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
  992. if (timer.ProductionYear.HasValue)
  993. {
  994. folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  995. }
  996. recordPath = Path.Combine(recordPath, folderName);
  997. }
  998. else if (timer.IsKids)
  999. {
  1000. if (config.EnableRecordingSubfolders)
  1001. {
  1002. recordPath = Path.Combine(recordPath, "Kids");
  1003. }
  1004. var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
  1005. if (timer.ProductionYear.HasValue)
  1006. {
  1007. folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  1008. }
  1009. recordPath = Path.Combine(recordPath, folderName);
  1010. }
  1011. else if (timer.IsSports)
  1012. {
  1013. if (config.EnableRecordingSubfolders)
  1014. {
  1015. recordPath = Path.Combine(recordPath, "Sports");
  1016. }
  1017. recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
  1018. }
  1019. else
  1020. {
  1021. if (config.EnableRecordingSubfolders)
  1022. {
  1023. recordPath = Path.Combine(recordPath, "Other");
  1024. }
  1025. recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
  1026. }
  1027. var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts";
  1028. return Path.Combine(recordPath, recordingFileName);
  1029. }
  1030. private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate,
  1031. ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
  1032. {
  1033. if (timer == null)
  1034. {
  1035. throw new ArgumentNullException("timer");
  1036. }
  1037. ProgramInfo programInfo = null;
  1038. if (!string.IsNullOrWhiteSpace(timer.ProgramId))
  1039. {
  1040. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  1041. }
  1042. if (programInfo == null)
  1043. {
  1044. _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
  1045. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
  1046. }
  1047. if (programInfo != null)
  1048. {
  1049. RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
  1050. activeRecordingInfo.Program = programInfo;
  1051. }
  1052. string seriesPath = null;
  1053. var recordPath = GetRecordingPath(timer, out seriesPath);
  1054. var recordingStatus = RecordingStatus.New;
  1055. string liveStreamId = null;
  1056. try
  1057. {
  1058. var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false);
  1059. var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None)
  1060. .ConfigureAwait(false);
  1061. var mediaStreamInfo = liveStreamInfo.Item2;
  1062. liveStreamId = mediaStreamInfo.Id;
  1063. // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
  1064. //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  1065. var recorder = await GetRecorder().ConfigureAwait(false);
  1066. recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath);
  1067. recordPath = EnsureFileUnique(recordPath, timer.Id);
  1068. _libraryManager.RegisterIgnoredPath(recordPath);
  1069. _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
  1070. _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
  1071. activeRecordingInfo.Path = recordPath;
  1072. var duration = recordingEndDate - DateTime.UtcNow;
  1073. _logger.Info("Beginning recording. Will record for {0} minutes.",
  1074. duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  1075. _logger.Info("Writing file to path: " + recordPath);
  1076. _logger.Info("Opening recording stream from tuner provider");
  1077. Action onStarted = () =>
  1078. {
  1079. timer.Status = RecordingStatus.InProgress;
  1080. _timerProvider.AddOrUpdate(timer, false);
  1081. SaveNfo(timer, recordPath, seriesPath);
  1082. EnforceKeepUpTo(timer);
  1083. };
  1084. await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken)
  1085. .ConfigureAwait(false);
  1086. recordingStatus = RecordingStatus.Completed;
  1087. _logger.Info("Recording completed: {0}", recordPath);
  1088. }
  1089. catch (OperationCanceledException)
  1090. {
  1091. _logger.Info("Recording stopped: {0}", recordPath);
  1092. recordingStatus = RecordingStatus.Completed;
  1093. }
  1094. catch (Exception ex)
  1095. {
  1096. _logger.ErrorException("Error recording to {0}", ex, recordPath);
  1097. recordingStatus = RecordingStatus.Error;
  1098. }
  1099. if (!string.IsNullOrWhiteSpace(liveStreamId))
  1100. {
  1101. try
  1102. {
  1103. await CloseLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
  1104. }
  1105. catch (Exception ex)
  1106. {
  1107. _logger.ErrorException("Error closing live stream", ex);
  1108. }
  1109. }
  1110. _libraryManager.UnRegisterIgnoredPath(recordPath);
  1111. _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
  1112. ActiveRecordingInfo removed;
  1113. _activeRecordings.TryRemove(timer.Id, out removed);
  1114. if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate)
  1115. {
  1116. const int retryIntervalSeconds = 60;
  1117. _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
  1118. timer.Status = RecordingStatus.New;
  1119. timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
  1120. _timerProvider.AddOrUpdate(timer);
  1121. }
  1122. else if (File.Exists(recordPath))
  1123. {
  1124. timer.RecordingPath = recordPath;
  1125. timer.Status = RecordingStatus.Completed;
  1126. _timerProvider.AddOrUpdate(timer, false);
  1127. OnSuccessfulRecording(timer, recordPath);
  1128. }
  1129. else
  1130. {
  1131. _timerProvider.Delete(timer);
  1132. }
  1133. }
  1134. private async void EnforceKeepUpTo(TimerInfo timer)
  1135. {
  1136. if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
  1137. {
  1138. return;
  1139. }
  1140. var seriesTimerId = timer.SeriesTimerId;
  1141. var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase));
  1142. if (seriesTimer == null || seriesTimer.KeepUpTo <= 1)
  1143. {
  1144. return;
  1145. }
  1146. if (_disposed)
  1147. {
  1148. return;
  1149. }
  1150. await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
  1151. try
  1152. {
  1153. if (_disposed)
  1154. {
  1155. return;
  1156. }
  1157. var timersToDelete = _timerProvider.GetAll()
  1158. .Where(i => i.Status == RecordingStatus.Completed && !string.IsNullOrWhiteSpace(i.RecordingPath))
  1159. .Where(i => string.Equals(i.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase))
  1160. .OrderByDescending(i => i.EndDate)
  1161. .Where(i => File.Exists(i.RecordingPath))
  1162. .Skip(seriesTimer.KeepUpTo - 1)
  1163. .ToList();
  1164. await DeleteLibraryItemsForTimers(timersToDelete).ConfigureAwait(false);
  1165. }
  1166. finally
  1167. {
  1168. _recordingDeleteSemaphore.Release();
  1169. }
  1170. }
  1171. private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
  1172. private async Task DeleteLibraryItemsForTimers(List<TimerInfo> timers)
  1173. {
  1174. foreach (var timer in timers)
  1175. {
  1176. if (_disposed)
  1177. {
  1178. return;
  1179. }
  1180. try
  1181. {
  1182. await DeleteLibraryItemForTimer(timer).ConfigureAwait(false);
  1183. }
  1184. catch (Exception ex)
  1185. {
  1186. _logger.ErrorException("Error deleting recording", ex);
  1187. }
  1188. }
  1189. }
  1190. private async Task DeleteLibraryItemForTimer(TimerInfo timer)
  1191. {
  1192. var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false);
  1193. if (libraryItem != null)
  1194. {
  1195. await _libraryManager.DeleteItem(libraryItem, new DeleteOptions
  1196. {
  1197. DeleteFileLocation = true
  1198. });
  1199. }
  1200. else
  1201. {
  1202. try
  1203. {
  1204. File.Delete(timer.RecordingPath);
  1205. }
  1206. catch (DirectoryNotFoundException)
  1207. {
  1208. }
  1209. catch (FileNotFoundException)
  1210. {
  1211. }
  1212. }
  1213. _timerProvider.Delete(timer);
  1214. }
  1215. private string EnsureFileUnique(string path, string timerId)
  1216. {
  1217. var originalPath = path;
  1218. var index = 1;
  1219. while (FileExists(path, timerId))
  1220. {
  1221. var parent = Path.GetDirectoryName(originalPath);
  1222. var name = Path.GetFileNameWithoutExtension(originalPath);
  1223. name += "-" + index.ToString(CultureInfo.InvariantCulture);
  1224. path = Path.ChangeExtension(Path.Combine(parent, name), Path.GetExtension(originalPath));
  1225. index++;
  1226. }
  1227. return path;
  1228. }
  1229. private bool FileExists(string path, string timerId)
  1230. {
  1231. if (_fileSystem.FileExists(path))
  1232. {
  1233. return true;
  1234. }
  1235. var hasRecordingAtPath = _activeRecordings
  1236. .Values
  1237. .ToList()
  1238. .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
  1239. if (hasRecordingAtPath)
  1240. {
  1241. return true;
  1242. }
  1243. return false;
  1244. }
  1245. private async Task<IRecorder> GetRecorder()
  1246. {
  1247. var config = GetConfiguration();
  1248. if (config.EnableRecordingEncoding)
  1249. {
  1250. var regInfo = await _liveTvManager.GetRegistrationInfo("embytvrecordingconversion").ConfigureAwait(false);
  1251. if (regInfo.IsValid)
  1252. {
  1253. return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config, _httpClient);
  1254. }
  1255. }
  1256. return new DirectRecorder(_logger, _httpClient, _fileSystem);
  1257. }
  1258. private async void OnSuccessfulRecording(TimerInfo timer, string path)
  1259. {
  1260. if (timer.IsProgramSeries && GetConfiguration().EnableAutoOrganize)
  1261. {
  1262. try
  1263. {
  1264. // this is to account for the library monitor holding a lock for additional time after the change is complete.
  1265. // ideally this shouldn't be hard-coded
  1266. await Task.Delay(30000).ConfigureAwait(false);
  1267. var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
  1268. var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false);
  1269. if (result.Status == FileSortingStatus.Success)
  1270. {
  1271. return;
  1272. }
  1273. }
  1274. catch (Exception ex)
  1275. {
  1276. _logger.ErrorException("Error processing new recording", ex);
  1277. }
  1278. }
  1279. }
  1280. private void SaveNfo(TimerInfo timer, string recordingPath, string seriesPath)
  1281. {
  1282. try
  1283. {
  1284. if (timer.IsProgramSeries)
  1285. {
  1286. SaveSeriesNfo(timer, recordingPath, seriesPath);
  1287. }
  1288. else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
  1289. {
  1290. SaveVideoNfo(timer, recordingPath);
  1291. }
  1292. }
  1293. catch (Exception ex)
  1294. {
  1295. _logger.ErrorException("Error saving nfo", ex);
  1296. }
  1297. }
  1298. private void SaveSeriesNfo(TimerInfo timer, string recordingPath, string seriesPath)
  1299. {
  1300. var nfoPath = Path.Combine(seriesPath, "tvshow.nfo");
  1301. if (File.Exists(nfoPath))
  1302. {
  1303. return;
  1304. }
  1305. using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
  1306. {
  1307. var settings = new XmlWriterSettings
  1308. {
  1309. Indent = true,
  1310. Encoding = Encoding.UTF8,
  1311. CloseOutput = false
  1312. };
  1313. using (XmlWriter writer = XmlWriter.Create(stream, settings))
  1314. {
  1315. writer.WriteStartDocument(true);
  1316. writer.WriteStartElement("tvshow");
  1317. if (!string.IsNullOrWhiteSpace(timer.Name))
  1318. {
  1319. writer.WriteElementString("title", timer.Name);
  1320. }
  1321. writer.WriteEndElement();
  1322. writer.WriteEndDocument();
  1323. }
  1324. }
  1325. }
  1326. public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
  1327. private void SaveVideoNfo(TimerInfo timer, string recordingPath)
  1328. {
  1329. var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
  1330. if (File.Exists(nfoPath))
  1331. {
  1332. return;
  1333. }
  1334. using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
  1335. {
  1336. var settings = new XmlWriterSettings
  1337. {
  1338. Indent = true,
  1339. Encoding = Encoding.UTF8,
  1340. CloseOutput = false
  1341. };
  1342. using (XmlWriter writer = XmlWriter.Create(stream, settings))
  1343. {
  1344. writer.WriteStartDocument(true);
  1345. writer.WriteStartElement("movie");
  1346. if (!string.IsNullOrWhiteSpace(timer.Name))
  1347. {
  1348. writer.WriteElementString("title", timer.Name);
  1349. }
  1350. writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
  1351. if (timer.ProductionYear.HasValue)
  1352. {
  1353. writer.WriteElementString("year", timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
  1354. }
  1355. if (!string.IsNullOrEmpty(timer.OfficialRating))
  1356. {
  1357. writer.WriteElementString("mpaa", timer.OfficialRating);
  1358. }
  1359. var overview = (timer.Overview ?? string.Empty)
  1360. .StripHtml()
  1361. .Replace("&quot;", "'");
  1362. writer.WriteElementString("plot", overview);
  1363. writer.WriteElementString("lockdata", true.ToString().ToLower());
  1364. if (timer.CommunityRating.HasValue)
  1365. {
  1366. writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
  1367. }
  1368. if (timer.IsSports)
  1369. {
  1370. AddGenre(timer.Genres, "Sports");
  1371. }
  1372. if (timer.IsKids)
  1373. {
  1374. AddGenre(timer.Genres, "Kids");
  1375. AddGenre(timer.Genres, "Children");
  1376. }
  1377. if (timer.IsNews)
  1378. {
  1379. AddGenre(timer.Genres, "News");
  1380. }
  1381. foreach (var genre in timer.Genres)
  1382. {
  1383. writer.WriteElementString("genre", genre);
  1384. }
  1385. if (!string.IsNullOrWhiteSpace(timer.ShortOverview))
  1386. {
  1387. writer.WriteElementString("outline", timer.ShortOverview);
  1388. }
  1389. if (!string.IsNullOrWhiteSpace(timer.HomePageUrl))
  1390. {
  1391. writer.WriteElementString("website", timer.HomePageUrl);
  1392. }
  1393. writer.WriteEndElement();
  1394. writer.WriteEndDocument();
  1395. }
  1396. }
  1397. }
  1398. private void AddGenre(List<string> genres, string genre)
  1399. {
  1400. if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
  1401. {
  1402. genres.Add(genre);
  1403. }
  1404. }
  1405. private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
  1406. {
  1407. var epgData = GetEpgDataForChannel(channelId);
  1408. return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
  1409. }
  1410. private ProgramInfo GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
  1411. {
  1412. var epgData = GetEpgDataForChannel(channelId);
  1413. var startDateTicks = startDateUtc.Ticks;
  1414. // Find the first program that starts within 3 minutes
  1415. return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
  1416. }
  1417. private LiveTvOptions GetConfiguration()
  1418. {
  1419. return _config.GetConfiguration<LiveTvOptions>("livetv");
  1420. }
  1421. private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
  1422. {
  1423. if (!seriesTimer.RecordAnyTime)
  1424. {
  1425. if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks)
  1426. {
  1427. return true;
  1428. }
  1429. if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
  1430. {
  1431. return true;
  1432. }
  1433. }
  1434. if (seriesTimer.RecordNewOnly && timer.IsRepeat)
  1435. {
  1436. return true;
  1437. }
  1438. if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
  1439. {
  1440. return true;
  1441. }
  1442. return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
  1443. }
  1444. private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
  1445. {
  1446. var allTimers = GetTimersForSeries(seriesTimer, epgData)
  1447. .ToList();
  1448. var registration = await _liveTvManager.GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
  1449. if (registration.IsValid)
  1450. {
  1451. foreach (var timer in allTimers)
  1452. {
  1453. var existingTimer = _timerProvider.GetTimer(timer.Id);
  1454. if (existingTimer == null)
  1455. {
  1456. if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
  1457. {
  1458. timer.Status = RecordingStatus.Cancelled;
  1459. }
  1460. _timerProvider.Add(timer);
  1461. }
  1462. else
  1463. {
  1464. // Only update if not currently active
  1465. ActiveRecordingInfo activeRecordingInfo;
  1466. if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo))
  1467. {
  1468. UpdateExistingTimerWithNewMetadata(existingTimer, timer);
  1469. if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
  1470. {
  1471. existingTimer.Status = RecordingStatus.Cancelled;
  1472. }
  1473. existingTimer.SeriesTimerId = seriesTimer.Id;
  1474. _timerProvider.Update(existingTimer);
  1475. }
  1476. }
  1477. }
  1478. }
  1479. if (deleteInvalidTimers)
  1480. {
  1481. var allTimerIds = allTimers
  1482. .Select(i => i.Id)
  1483. .ToList();
  1484. var deleteStatuses = new List<RecordingStatus>
  1485. {
  1486. RecordingStatus.New
  1487. };
  1488. var deletes = _timerProvider.GetAll()
  1489. .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
  1490. .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
  1491. .Where(i => deleteStatuses.Contains(i.Status))
  1492. .ToList();
  1493. foreach (var timer in deletes)
  1494. {
  1495. CancelTimerInternal(timer.Id, false);
  1496. }
  1497. }
  1498. }
  1499. private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer,
  1500. IEnumerable<ProgramInfo> allPrograms)
  1501. {
  1502. if (seriesTimer == null)
  1503. {
  1504. throw new ArgumentNullException("seriesTimer");
  1505. }
  1506. if (allPrograms == null)
  1507. {
  1508. throw new ArgumentNullException("allPrograms");
  1509. }
  1510. // Exclude programs that have already ended
  1511. allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow);
  1512. allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
  1513. return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
  1514. }
  1515. private bool IsProgramAlreadyInLibrary(TimerInfo program)
  1516. {
  1517. if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
  1518. {
  1519. var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
  1520. {
  1521. IncludeItemTypes = new[] { typeof(Series).Name },
  1522. Name = program.Name
  1523. }).Select(i => i.ToString("N")).ToArray();
  1524. if (seriesIds.Length == 0)
  1525. {
  1526. return false;
  1527. }
  1528. if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
  1529. {
  1530. var result = _libraryManager.GetItemsResult(new InternalItemsQuery
  1531. {
  1532. IncludeItemTypes = new[] { typeof(Episode).Name },
  1533. ParentIndexNumber = program.SeasonNumber.Value,
  1534. IndexNumber = program.EpisodeNumber.Value,
  1535. AncestorIds = seriesIds,
  1536. ExcludeLocationTypes = new[] { LocationType.Virtual }
  1537. });
  1538. if (result.TotalRecordCount > 0)
  1539. {
  1540. return true;
  1541. }
  1542. }
  1543. if (!string.IsNullOrWhiteSpace(program.EpisodeTitle))
  1544. {
  1545. var result = _libraryManager.GetItemsResult(new InternalItemsQuery
  1546. {
  1547. IncludeItemTypes = new[] { typeof(Episode).Name },
  1548. Name = program.EpisodeTitle,
  1549. AncestorIds = seriesIds,
  1550. ExcludeLocationTypes = new[] { LocationType.Virtual }
  1551. });
  1552. if (result.TotalRecordCount > 0)
  1553. {
  1554. return true;
  1555. }
  1556. }
  1557. }
  1558. return false;
  1559. }
  1560. private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
  1561. {
  1562. if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
  1563. {
  1564. _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
  1565. return new List<ProgramInfo>();
  1566. }
  1567. return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
  1568. }
  1569. private string GetChannelEpgCachePath(string channelId)
  1570. {
  1571. return Path.Combine(_config.CommonApplicationPaths.CachePath, "embytvepg", channelId + ".json");
  1572. }
  1573. private readonly object _epgLock = new object();
  1574. private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
  1575. {
  1576. var path = GetChannelEpgCachePath(channelId);
  1577. _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
  1578. lock (_epgLock)
  1579. {
  1580. _jsonSerializer.SerializeToFile(epgData, path);
  1581. }
  1582. }
  1583. private List<ProgramInfo> GetEpgDataForChannel(string channelId)
  1584. {
  1585. try
  1586. {
  1587. lock (_epgLock)
  1588. {
  1589. return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
  1590. }
  1591. }
  1592. catch
  1593. {
  1594. return new List<ProgramInfo>();
  1595. }
  1596. }
  1597. private List<ProgramInfo> GetEpgDataForChannels(List<string> channelIds)
  1598. {
  1599. return channelIds.SelectMany(GetEpgDataForChannel).ToList();
  1600. }
  1601. private bool _disposed;
  1602. public void Dispose()
  1603. {
  1604. _disposed = true;
  1605. foreach (var pair in _activeRecordings.ToList())
  1606. {
  1607. pair.Value.CancellationTokenSource.Cancel();
  1608. }
  1609. }
  1610. public List<VirtualFolderInfo> GetRecordingFolders()
  1611. {
  1612. var list = new List<VirtualFolderInfo>();
  1613. var defaultFolder = RecordingPath;
  1614. var defaultName = "Recordings";
  1615. if (Directory.Exists(defaultFolder))
  1616. {
  1617. list.Add(new VirtualFolderInfo
  1618. {
  1619. Locations = new List<string> { defaultFolder },
  1620. Name = defaultName
  1621. });
  1622. }
  1623. var customPath = GetConfiguration().MovieRecordingPath;
  1624. if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
  1625. {
  1626. list.Add(new VirtualFolderInfo
  1627. {
  1628. Locations = new List<string> { customPath },
  1629. Name = "Recorded Movies",
  1630. CollectionType = CollectionType.Movies
  1631. });
  1632. }
  1633. customPath = GetConfiguration().SeriesRecordingPath;
  1634. if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
  1635. {
  1636. list.Add(new VirtualFolderInfo
  1637. {
  1638. Locations = new List<string> { customPath },
  1639. Name = "Recorded Series",
  1640. CollectionType = CollectionType.TvShows
  1641. });
  1642. }
  1643. return list;
  1644. }
  1645. class ActiveRecordingInfo
  1646. {
  1647. public string Path { get; set; }
  1648. public TimerInfo Timer { get; set; }
  1649. public ProgramInfo Program { get; set; }
  1650. public CancellationTokenSource CancellationTokenSource { get; set; }
  1651. }
  1652. }
  1653. }