EmbyTV.cs 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345
  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.FileOrganization;
  16. using MediaBrowser.Model.LiveTv;
  17. using MediaBrowser.Model.Logging;
  18. using MediaBrowser.Model.Serialization;
  19. using MediaBrowser.Server.Implementations.FileOrganization;
  20. using System;
  21. using System.Collections.Concurrent;
  22. using System.Collections.Generic;
  23. using System.Globalization;
  24. using System.IO;
  25. using System.Linq;
  26. using System.Threading;
  27. using System.Threading.Tasks;
  28. using CommonIO;
  29. using MediaBrowser.Common.Events;
  30. using MediaBrowser.Common.Extensions;
  31. using MediaBrowser.Controller.Entities;
  32. using MediaBrowser.Controller.Entities.TV;
  33. using MediaBrowser.Controller.Power;
  34. using Microsoft.Win32;
  35. namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
  36. {
  37. public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable
  38. {
  39. private readonly IApplicationHost _appHpst;
  40. private readonly ILogger _logger;
  41. private readonly IHttpClient _httpClient;
  42. private readonly IServerConfigurationManager _config;
  43. private readonly IJsonSerializer _jsonSerializer;
  44. private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
  45. private readonly TimerManager _timerProvider;
  46. private readonly LiveTvManager _liveTvManager;
  47. private readonly IFileSystem _fileSystem;
  48. private readonly ISecurityManager _security;
  49. private readonly ILibraryMonitor _libraryMonitor;
  50. private readonly ILibraryManager _libraryManager;
  51. private readonly IProviderManager _providerManager;
  52. private readonly IFileOrganizationService _organizationService;
  53. private readonly IMediaEncoder _mediaEncoder;
  54. public static EmbyTV Current;
  55. public event EventHandler DataSourceChanged;
  56. public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
  57. private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
  58. new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
  59. public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement)
  60. {
  61. Current = this;
  62. _appHpst = appHost;
  63. _logger = logger;
  64. _httpClient = httpClient;
  65. _config = config;
  66. _fileSystem = fileSystem;
  67. _security = security;
  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"), powerManagement, _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. try
  125. {
  126. _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), true);
  127. }
  128. catch (Exception ex)
  129. {
  130. _logger.ErrorException("Error creating virtual folder", ex);
  131. }
  132. pathsAdded.AddRange(pathsToCreate);
  133. }
  134. var config = GetConfiguration();
  135. var pathsToRemove = config.MediaLocationsCreated
  136. .Except(recordingFolders.SelectMany(i => i.Locations))
  137. .ToList();
  138. if (pathsAdded.Count > 0 || pathsToRemove.Count > 0)
  139. {
  140. pathsAdded.InsertRange(0, config.MediaLocationsCreated);
  141. config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  142. _config.SaveConfiguration("livetv", config);
  143. }
  144. foreach (var path in pathsToRemove)
  145. {
  146. RemovePathFromLibrary(path);
  147. }
  148. }
  149. private void RemovePathFromLibrary(string path)
  150. {
  151. _logger.Debug("Removing path from library: {0}", path);
  152. var requiresRefresh = false;
  153. var virtualFolders = _libraryManager.GetVirtualFolders()
  154. .ToList();
  155. foreach (var virtualFolder in virtualFolders)
  156. {
  157. if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
  158. {
  159. continue;
  160. }
  161. if (virtualFolder.Locations.Count == 1)
  162. {
  163. // remove entire virtual folder
  164. try
  165. {
  166. _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true);
  167. }
  168. catch (Exception ex)
  169. {
  170. _logger.ErrorException("Error removing virtual folder", ex);
  171. }
  172. }
  173. else
  174. {
  175. try
  176. {
  177. _libraryManager.RemoveMediaPath(virtualFolder.Name, path);
  178. requiresRefresh = true;
  179. }
  180. catch (Exception ex)
  181. {
  182. _logger.ErrorException("Error removing media path", ex);
  183. }
  184. }
  185. }
  186. if (requiresRefresh)
  187. {
  188. _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None);
  189. }
  190. }
  191. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  192. {
  193. _logger.Info("Power mode changed to {0}", e.Mode);
  194. if (e.Mode == PowerModes.Resume)
  195. {
  196. _timerProvider.RestartTimers();
  197. }
  198. }
  199. public string Name
  200. {
  201. get { return "Emby"; }
  202. }
  203. public string DataPath
  204. {
  205. get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
  206. }
  207. private string DefaultRecordingPath
  208. {
  209. get
  210. {
  211. return Path.Combine(DataPath, "recordings");
  212. }
  213. }
  214. private string RecordingPath
  215. {
  216. get
  217. {
  218. var path = GetConfiguration().RecordingPath;
  219. return string.IsNullOrWhiteSpace(path)
  220. ? DefaultRecordingPath
  221. : path;
  222. }
  223. }
  224. public string HomePageUrl
  225. {
  226. get { return "http://emby.media"; }
  227. }
  228. public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
  229. {
  230. var status = new LiveTvServiceStatusInfo();
  231. var list = new List<LiveTvTunerInfo>();
  232. foreach (var hostInstance in _liveTvManager.TunerHosts)
  233. {
  234. try
  235. {
  236. var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
  237. list.AddRange(tuners);
  238. }
  239. catch (Exception ex)
  240. {
  241. _logger.ErrorException("Error getting tuners", ex);
  242. }
  243. }
  244. status.Tuners = list;
  245. status.Status = LiveTvServiceStatus.Ok;
  246. status.Version = _appHpst.ApplicationVersion.ToString();
  247. status.IsVisible = false;
  248. return status;
  249. }
  250. public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
  251. {
  252. var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
  253. List<ChannelInfo> channels = null;
  254. foreach (var timer in seriesTimers)
  255. {
  256. List<ProgramInfo> epgData;
  257. if (timer.RecordAnyChannel)
  258. {
  259. if (channels == null)
  260. {
  261. channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList();
  262. }
  263. var channelIds = channels.Select(i => i.Id).ToList();
  264. epgData = GetEpgDataForChannels(channelIds);
  265. }
  266. else
  267. {
  268. epgData = GetEpgDataForChannel(timer.ChannelId);
  269. }
  270. await UpdateTimersForSeriesTimer(epgData, timer, true).ConfigureAwait(false);
  271. }
  272. var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
  273. foreach (var timer in timers.ToList())
  274. {
  275. if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
  276. {
  277. _timerProvider.Delete(timer);
  278. }
  279. }
  280. }
  281. private List<ChannelInfo> _channelCache = null;
  282. private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
  283. {
  284. if (enableCache && _channelCache != null)
  285. {
  286. return _channelCache.ToList();
  287. }
  288. var list = new List<ChannelInfo>();
  289. foreach (var hostInstance in _liveTvManager.TunerHosts)
  290. {
  291. try
  292. {
  293. var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
  294. list.AddRange(channels);
  295. }
  296. catch (Exception ex)
  297. {
  298. _logger.ErrorException("Error getting channels", ex);
  299. }
  300. }
  301. foreach (var provider in GetListingProviders())
  302. {
  303. var enabledChannels = list
  304. .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
  305. .ToList();
  306. if (enabledChannels.Count > 0)
  307. {
  308. try
  309. {
  310. await provider.Item1.AddMetadata(provider.Item2, enabledChannels, cancellationToken).ConfigureAwait(false);
  311. }
  312. catch (NotSupportedException)
  313. {
  314. }
  315. catch (Exception ex)
  316. {
  317. _logger.ErrorException("Error adding metadata", ex);
  318. }
  319. }
  320. }
  321. _channelCache = list.ToList();
  322. return list;
  323. }
  324. public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
  325. {
  326. return GetChannelsAsync(false, cancellationToken);
  327. }
  328. public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
  329. {
  330. var timers = _timerProvider
  331. .GetAll()
  332. .Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
  333. .ToList();
  334. foreach (var timer in timers)
  335. {
  336. CancelTimerInternal(timer.Id);
  337. }
  338. var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  339. if (remove != null)
  340. {
  341. _seriesTimerProvider.Delete(remove);
  342. }
  343. return Task.FromResult(true);
  344. }
  345. private void CancelTimerInternal(string timerId)
  346. {
  347. var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  348. if (remove != null)
  349. {
  350. _timerProvider.Delete(remove);
  351. }
  352. ActiveRecordingInfo activeRecordingInfo;
  353. if (_activeRecordings.TryGetValue(timerId, out activeRecordingInfo))
  354. {
  355. activeRecordingInfo.CancellationTokenSource.Cancel();
  356. }
  357. }
  358. public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
  359. {
  360. CancelTimerInternal(timerId);
  361. return Task.FromResult(true);
  362. }
  363. public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
  364. {
  365. return Task.FromResult(true);
  366. }
  367. public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  368. {
  369. info.Id = Guid.NewGuid().ToString("N");
  370. _timerProvider.Add(info);
  371. return Task.FromResult(0);
  372. }
  373. public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  374. {
  375. info.Id = Guid.NewGuid().ToString("N");
  376. List<ProgramInfo> epgData;
  377. if (info.RecordAnyChannel)
  378. {
  379. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  380. var channelIds = channels.Select(i => i.Id).ToList();
  381. epgData = GetEpgDataForChannels(channelIds);
  382. }
  383. else
  384. {
  385. epgData = GetEpgDataForChannel(info.ChannelId);
  386. }
  387. // populate info.seriesID
  388. var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
  389. if (program != null)
  390. {
  391. info.SeriesId = program.SeriesId;
  392. }
  393. else
  394. {
  395. throw new InvalidOperationException("SeriesId for program not found");
  396. }
  397. _seriesTimerProvider.Add(info);
  398. await UpdateTimersForSeriesTimer(epgData, info, false).ConfigureAwait(false);
  399. }
  400. public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  401. {
  402. var instance = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
  403. if (instance != null)
  404. {
  405. instance.ChannelId = info.ChannelId;
  406. instance.Days = info.Days;
  407. instance.EndDate = info.EndDate;
  408. instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
  409. instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
  410. instance.PostPaddingSeconds = info.PostPaddingSeconds;
  411. instance.PrePaddingSeconds = info.PrePaddingSeconds;
  412. instance.Priority = info.Priority;
  413. instance.RecordAnyChannel = info.RecordAnyChannel;
  414. instance.RecordAnyTime = info.RecordAnyTime;
  415. instance.RecordNewOnly = info.RecordNewOnly;
  416. instance.StartDate = info.StartDate;
  417. _seriesTimerProvider.Update(instance);
  418. List<ProgramInfo> epgData;
  419. if (instance.RecordAnyChannel)
  420. {
  421. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  422. var channelIds = channels.Select(i => i.Id).ToList();
  423. epgData = GetEpgDataForChannels(channelIds);
  424. }
  425. else
  426. {
  427. epgData = GetEpgDataForChannel(instance.ChannelId);
  428. }
  429. await UpdateTimersForSeriesTimer(epgData, instance, true).ConfigureAwait(false);
  430. }
  431. }
  432. public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  433. {
  434. _timerProvider.Update(info);
  435. return Task.FromResult(true);
  436. }
  437. public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
  438. {
  439. throw new NotImplementedException();
  440. }
  441. public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
  442. {
  443. throw new NotImplementedException();
  444. }
  445. public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
  446. {
  447. throw new NotImplementedException();
  448. }
  449. public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
  450. {
  451. return new List<RecordingInfo>();
  452. }
  453. public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
  454. {
  455. return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
  456. }
  457. public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
  458. {
  459. var config = GetConfiguration();
  460. var defaults = new SeriesTimerInfo()
  461. {
  462. PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
  463. PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
  464. RecordAnyChannel = false,
  465. RecordAnyTime = false,
  466. RecordNewOnly = false
  467. };
  468. if (program != null)
  469. {
  470. defaults.SeriesId = program.SeriesId;
  471. defaults.ProgramId = program.Id;
  472. }
  473. return Task.FromResult(defaults);
  474. }
  475. public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
  476. {
  477. return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
  478. }
  479. public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  480. {
  481. try
  482. {
  483. return await GetProgramsAsyncInternal(channelId, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false);
  484. }
  485. catch (OperationCanceledException)
  486. {
  487. throw;
  488. }
  489. catch (Exception ex)
  490. {
  491. _logger.ErrorException("Error getting programs", ex);
  492. return GetEpgDataForChannel(channelId).Where(i => i.StartDate <= endDateUtc && i.EndDate >= startDateUtc);
  493. }
  494. }
  495. private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
  496. {
  497. if (info.EnableAllTuners)
  498. {
  499. return true;
  500. }
  501. if (string.IsNullOrWhiteSpace(tunerHostId))
  502. {
  503. throw new ArgumentNullException("tunerHostId");
  504. }
  505. return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
  506. }
  507. private async Task<IEnumerable<ProgramInfo>> GetProgramsAsyncInternal(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  508. {
  509. var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
  510. var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  511. foreach (var provider in GetListingProviders())
  512. {
  513. if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
  514. {
  515. _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);
  516. continue;
  517. }
  518. _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
  519. var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken)
  520. .ConfigureAwait(false);
  521. var list = programs.ToList();
  522. // Replace the value that came from the provider with a normalized value
  523. foreach (var program in list)
  524. {
  525. program.ChannelId = channelId;
  526. }
  527. if (list.Count > 0)
  528. {
  529. SaveEpgDataForChannel(channelId, list);
  530. return list;
  531. }
  532. }
  533. return new List<ProgramInfo>();
  534. }
  535. private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
  536. {
  537. return GetConfiguration().ListingProviders
  538. .Select(i =>
  539. {
  540. var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
  541. return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
  542. })
  543. .Where(i => i != null)
  544. .ToList();
  545. }
  546. public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
  547. {
  548. throw new NotImplementedException();
  549. }
  550. public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
  551. {
  552. _logger.Info("Streaming Channel " + channelId);
  553. foreach (var hostInstance in _liveTvManager.TunerHosts)
  554. {
  555. try
  556. {
  557. var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  558. result.Item2.Release();
  559. return result.Item1;
  560. }
  561. catch (Exception e)
  562. {
  563. _logger.ErrorException("Error getting channel stream", e);
  564. }
  565. }
  566. throw new ApplicationException("Tuner not found.");
  567. }
  568. private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
  569. {
  570. _logger.Info("Streaming Channel " + channelId);
  571. foreach (var hostInstance in _liveTvManager.TunerHosts)
  572. {
  573. try
  574. {
  575. var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  576. return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2);
  577. }
  578. catch (Exception e)
  579. {
  580. _logger.ErrorException("Error getting channel stream", e);
  581. }
  582. }
  583. throw new ApplicationException("Tuner not found.");
  584. }
  585. public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
  586. {
  587. foreach (var hostInstance in _liveTvManager.TunerHosts)
  588. {
  589. try
  590. {
  591. var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
  592. if (sources.Count > 0)
  593. {
  594. return sources;
  595. }
  596. }
  597. catch (NotImplementedException)
  598. {
  599. }
  600. }
  601. throw new NotImplementedException();
  602. }
  603. public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
  604. {
  605. throw new NotImplementedException();
  606. }
  607. public Task CloseLiveStream(string id, CancellationToken cancellationToken)
  608. {
  609. return Task.FromResult(0);
  610. }
  611. public Task RecordLiveStream(string id, CancellationToken cancellationToken)
  612. {
  613. return Task.FromResult(0);
  614. }
  615. public Task ResetTuner(string id, CancellationToken cancellationToken)
  616. {
  617. return Task.FromResult(0);
  618. }
  619. async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
  620. {
  621. var timer = e.Argument;
  622. _logger.Info("Recording timer fired.");
  623. try
  624. {
  625. var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
  626. if (recordingEndDate <= DateTime.UtcNow)
  627. {
  628. _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
  629. return;
  630. }
  631. var activeRecordingInfo = new ActiveRecordingInfo
  632. {
  633. CancellationTokenSource = new CancellationTokenSource(),
  634. TimerId = timer.Id
  635. };
  636. if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
  637. {
  638. await RecordStream(timer, recordingEndDate, activeRecordingInfo, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
  639. }
  640. else
  641. {
  642. _logger.Info("Skipping RecordStream because it's already in progress.");
  643. }
  644. }
  645. catch (OperationCanceledException)
  646. {
  647. }
  648. catch (Exception ex)
  649. {
  650. _logger.ErrorException("Error recording stream", ex);
  651. }
  652. }
  653. private string GetRecordingPath(TimerInfo timer, ProgramInfo info)
  654. {
  655. var recordPath = RecordingPath;
  656. var config = GetConfiguration();
  657. if (info.IsMovie)
  658. {
  659. var customRecordingPath = config.MovieRecordingPath;
  660. var allowSubfolder = true;
  661. if (!string.IsNullOrWhiteSpace(customRecordingPath))
  662. {
  663. allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
  664. recordPath = customRecordingPath;
  665. }
  666. if (allowSubfolder && config.EnableRecordingSubfolders)
  667. {
  668. recordPath = Path.Combine(recordPath, "Movies");
  669. }
  670. var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
  671. if (info.ProductionYear.HasValue)
  672. {
  673. folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  674. }
  675. recordPath = Path.Combine(recordPath, folderName);
  676. }
  677. else if (info.IsSeries)
  678. {
  679. var customRecordingPath = config.SeriesRecordingPath;
  680. var allowSubfolder = true;
  681. if (!string.IsNullOrWhiteSpace(customRecordingPath))
  682. {
  683. allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
  684. recordPath = customRecordingPath;
  685. }
  686. if (allowSubfolder && config.EnableRecordingSubfolders)
  687. {
  688. recordPath = Path.Combine(recordPath, "Series");
  689. }
  690. var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
  691. var folderNameWithYear = folderName;
  692. if (info.ProductionYear.HasValue)
  693. {
  694. folderNameWithYear += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  695. }
  696. if (Directory.Exists(Path.Combine(recordPath, folderName)))
  697. {
  698. recordPath = Path.Combine(recordPath, folderName);
  699. }
  700. else
  701. {
  702. recordPath = Path.Combine(recordPath, folderNameWithYear);
  703. }
  704. if (info.SeasonNumber.HasValue)
  705. {
  706. folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
  707. recordPath = Path.Combine(recordPath, folderName);
  708. }
  709. }
  710. else if (info.IsKids)
  711. {
  712. if (config.EnableRecordingSubfolders)
  713. {
  714. recordPath = Path.Combine(recordPath, "Kids");
  715. }
  716. var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
  717. if (info.ProductionYear.HasValue)
  718. {
  719. folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  720. }
  721. recordPath = Path.Combine(recordPath, folderName);
  722. }
  723. else if (info.IsSports)
  724. {
  725. if (config.EnableRecordingSubfolders)
  726. {
  727. recordPath = Path.Combine(recordPath, "Sports");
  728. }
  729. recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
  730. }
  731. else
  732. {
  733. if (config.EnableRecordingSubfolders)
  734. {
  735. recordPath = Path.Combine(recordPath, "Other");
  736. }
  737. recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
  738. }
  739. var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
  740. return Path.Combine(recordPath, recordingFileName);
  741. }
  742. private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
  743. {
  744. if (timer == null)
  745. {
  746. throw new ArgumentNullException("timer");
  747. }
  748. ProgramInfo info = null;
  749. if (string.IsNullOrWhiteSpace(timer.ProgramId))
  750. {
  751. _logger.Info("Timer {0} has null programId", timer.Id);
  752. }
  753. else
  754. {
  755. info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  756. }
  757. if (info == null)
  758. {
  759. _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
  760. info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
  761. }
  762. if (info == null)
  763. {
  764. throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
  765. }
  766. var recordPath = GetRecordingPath(timer, info);
  767. var recordingStatus = RecordingStatus.New;
  768. try
  769. {
  770. var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
  771. var mediaStreamInfo = result.Item1;
  772. var isResourceOpen = true;
  773. // Unfortunately due to the semaphore we have to have a nested try/finally
  774. try
  775. {
  776. // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
  777. //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  778. var recorder = await GetRecorder().ConfigureAwait(false);
  779. recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath);
  780. recordPath = EnsureFileUnique(recordPath, timer.Id);
  781. _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
  782. _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
  783. activeRecordingInfo.Path = recordPath;
  784. var duration = recordingEndDate - DateTime.UtcNow;
  785. _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  786. _logger.Info("Writing file to path: " + recordPath);
  787. _logger.Info("Opening recording stream from tuner provider");
  788. Action onStarted = () =>
  789. {
  790. result.Item3.Release();
  791. isResourceOpen = false;
  792. };
  793. var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration);
  794. // If it supports supplying duration via url
  795. if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
  796. {
  797. mediaStreamInfo.Path = pathWithDuration;
  798. mediaStreamInfo.RunTimeTicks = duration.Ticks;
  799. }
  800. await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
  801. recordingStatus = RecordingStatus.Completed;
  802. _logger.Info("Recording completed: {0}", recordPath);
  803. }
  804. finally
  805. {
  806. if (isResourceOpen)
  807. {
  808. result.Item3.Release();
  809. }
  810. _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false);
  811. }
  812. }
  813. catch (OperationCanceledException)
  814. {
  815. _logger.Info("Recording stopped: {0}", recordPath);
  816. recordingStatus = RecordingStatus.Completed;
  817. }
  818. catch (Exception ex)
  819. {
  820. _logger.ErrorException("Error recording to {0}", ex, recordPath);
  821. recordingStatus = RecordingStatus.Error;
  822. }
  823. finally
  824. {
  825. ActiveRecordingInfo removed;
  826. _activeRecordings.TryRemove(timer.Id, out removed);
  827. }
  828. if (recordingStatus == RecordingStatus.Completed)
  829. {
  830. OnSuccessfulRecording(info.IsSeries, recordPath);
  831. _timerProvider.Delete(timer);
  832. }
  833. else if (DateTime.UtcNow < timer.EndDate)
  834. {
  835. const int retryIntervalSeconds = 60;
  836. _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
  837. _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds));
  838. }
  839. else
  840. {
  841. _timerProvider.Delete(timer);
  842. }
  843. }
  844. private string EnsureFileUnique(string path, string timerId)
  845. {
  846. var originalPath = path;
  847. var index = 1;
  848. while (FileExists(path, timerId))
  849. {
  850. var parent = Path.GetDirectoryName(originalPath);
  851. var name = Path.GetFileNameWithoutExtension(originalPath);
  852. name += "-" + index.ToString(CultureInfo.InvariantCulture);
  853. path = Path.ChangeExtension(Path.Combine(parent, name), Path.GetExtension(originalPath));
  854. index++;
  855. }
  856. return path;
  857. }
  858. private bool FileExists(string path, string timerId)
  859. {
  860. if (_fileSystem.FileExists(path))
  861. {
  862. return true;
  863. }
  864. var hasRecordingAtPath = _activeRecordings.Values.ToList().Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.TimerId, timerId, StringComparison.OrdinalIgnoreCase));
  865. if (hasRecordingAtPath)
  866. {
  867. return true;
  868. }
  869. return false;
  870. }
  871. private async Task<IRecorder> GetRecorder()
  872. {
  873. var config = GetConfiguration();
  874. if (config.EnableRecordingEncoding)
  875. {
  876. var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false);
  877. if (regInfo.IsValid)
  878. {
  879. return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config);
  880. }
  881. }
  882. return new DirectRecorder(_logger, _httpClient, _fileSystem);
  883. }
  884. private async void OnSuccessfulRecording(bool isSeries, string path)
  885. {
  886. if (GetConfiguration().EnableAutoOrganize)
  887. {
  888. if (isSeries)
  889. {
  890. try
  891. {
  892. // this is to account for the library monitor holding a lock for additional time after the change is complete.
  893. // ideally this shouldn't be hard-coded
  894. await Task.Delay(30000).ConfigureAwait(false);
  895. var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
  896. var result = await organize.OrganizeEpisodeFile(path, CancellationToken.None).ConfigureAwait(false);
  897. }
  898. catch (Exception ex)
  899. {
  900. _logger.ErrorException("Error processing new recording", ex);
  901. }
  902. }
  903. }
  904. }
  905. private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
  906. {
  907. var epgData = GetEpgDataForChannel(channelId);
  908. return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
  909. }
  910. private ProgramInfo GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
  911. {
  912. var epgData = GetEpgDataForChannel(channelId);
  913. var startDateTicks = startDateUtc.Ticks;
  914. // Find the first program that starts within 3 minutes
  915. return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
  916. }
  917. private LiveTvOptions GetConfiguration()
  918. {
  919. return _config.GetConfiguration<LiveTvOptions>("livetv");
  920. }
  921. private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
  922. {
  923. var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList();
  924. var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
  925. if (registration.IsValid)
  926. {
  927. foreach (var timer in newTimers)
  928. {
  929. _timerProvider.AddOrUpdate(timer);
  930. }
  931. }
  932. if (deleteInvalidTimers)
  933. {
  934. var allTimers = GetTimersForSeries(seriesTimer, epgData, false)
  935. .Select(i => i.Id)
  936. .ToList();
  937. var deletes = _timerProvider.GetAll()
  938. .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
  939. .Where(i => !allTimers.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
  940. .ToList();
  941. foreach (var timer in deletes)
  942. {
  943. await CancelTimerAsync(timer.Id, CancellationToken.None).ConfigureAwait(false);
  944. }
  945. }
  946. }
  947. private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer,
  948. IEnumerable<ProgramInfo> allPrograms,
  949. bool filterByCurrentRecordings)
  950. {
  951. if (seriesTimer == null)
  952. {
  953. throw new ArgumentNullException("seriesTimer");
  954. }
  955. if (allPrograms == null)
  956. {
  957. throw new ArgumentNullException("allPrograms");
  958. }
  959. // Exclude programs that have already ended
  960. allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
  961. allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
  962. if (filterByCurrentRecordings)
  963. {
  964. allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i));
  965. }
  966. return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
  967. }
  968. private bool IsProgramAlreadyInLibrary(ProgramInfo program)
  969. {
  970. if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
  971. {
  972. var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
  973. {
  974. IncludeItemTypes = new[] { typeof(Series).Name },
  975. Name = program.Name
  976. }).Select(i => i.ToString("N")).ToArray();
  977. if (seriesIds.Length == 0)
  978. {
  979. return false;
  980. }
  981. if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
  982. {
  983. var result = _libraryManager.GetItemsResult(new InternalItemsQuery
  984. {
  985. IncludeItemTypes = new[] { typeof(Episode).Name },
  986. ParentIndexNumber = program.SeasonNumber.Value,
  987. IndexNumber = program.EpisodeNumber.Value,
  988. AncestorIds = seriesIds,
  989. ExcludeLocationTypes = new[] { LocationType.Virtual }
  990. });
  991. if (result.TotalRecordCount > 0)
  992. {
  993. return true;
  994. }
  995. }
  996. if (!string.IsNullOrWhiteSpace(program.EpisodeTitle))
  997. {
  998. var result = _libraryManager.GetItemsResult(new InternalItemsQuery
  999. {
  1000. IncludeItemTypes = new[] { typeof(Episode).Name },
  1001. Name = program.EpisodeTitle,
  1002. AncestorIds = seriesIds,
  1003. ExcludeLocationTypes = new[] { LocationType.Virtual }
  1004. });
  1005. if (result.TotalRecordCount > 0)
  1006. {
  1007. return true;
  1008. }
  1009. }
  1010. }
  1011. return false;
  1012. }
  1013. private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
  1014. {
  1015. if (!seriesTimer.RecordAnyTime)
  1016. {
  1017. allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks);
  1018. }
  1019. if (seriesTimer.RecordNewOnly)
  1020. {
  1021. allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
  1022. }
  1023. if (!seriesTimer.RecordAnyChannel)
  1024. {
  1025. allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
  1026. }
  1027. allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
  1028. if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
  1029. {
  1030. _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
  1031. return new List<ProgramInfo>();
  1032. }
  1033. return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
  1034. }
  1035. private string GetChannelEpgCachePath(string channelId)
  1036. {
  1037. return Path.Combine(_config.CommonApplicationPaths.CachePath, "embytvepg", channelId + ".json");
  1038. }
  1039. private readonly object _epgLock = new object();
  1040. private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
  1041. {
  1042. var path = GetChannelEpgCachePath(channelId);
  1043. _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
  1044. lock (_epgLock)
  1045. {
  1046. _jsonSerializer.SerializeToFile(epgData, path);
  1047. }
  1048. }
  1049. private List<ProgramInfo> GetEpgDataForChannel(string channelId)
  1050. {
  1051. try
  1052. {
  1053. lock (_epgLock)
  1054. {
  1055. return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
  1056. }
  1057. }
  1058. catch
  1059. {
  1060. return new List<ProgramInfo>();
  1061. }
  1062. }
  1063. private List<ProgramInfo> GetEpgDataForChannels(List<string> channelIds)
  1064. {
  1065. return channelIds.SelectMany(GetEpgDataForChannel).ToList();
  1066. }
  1067. public void Dispose()
  1068. {
  1069. foreach (var pair in _activeRecordings.ToList())
  1070. {
  1071. pair.Value.CancellationTokenSource.Cancel();
  1072. }
  1073. }
  1074. public Task<MBRegistrationRecord> GetRegistrationInfo(string feature)
  1075. {
  1076. if (string.Equals(feature, "seriesrecordings", StringComparison.OrdinalIgnoreCase))
  1077. {
  1078. return _security.GetRegistrationStatus("embytvseriesrecordings");
  1079. }
  1080. return Task.FromResult(new MBRegistrationRecord
  1081. {
  1082. IsValid = true,
  1083. IsRegistered = true
  1084. });
  1085. }
  1086. public List<VirtualFolderInfo> GetRecordingFolders()
  1087. {
  1088. var list = new List<VirtualFolderInfo>();
  1089. var defaultFolder = RecordingPath;
  1090. var defaultName = "Recordings";
  1091. if (Directory.Exists(defaultFolder))
  1092. {
  1093. list.Add(new VirtualFolderInfo
  1094. {
  1095. Locations = new List<string> { defaultFolder },
  1096. Name = defaultName
  1097. });
  1098. }
  1099. var customPath = GetConfiguration().MovieRecordingPath;
  1100. if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
  1101. {
  1102. list.Add(new VirtualFolderInfo
  1103. {
  1104. Locations = new List<string> { customPath },
  1105. Name = "Recorded Movies",
  1106. CollectionType = CollectionType.Movies
  1107. });
  1108. }
  1109. customPath = GetConfiguration().SeriesRecordingPath;
  1110. if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
  1111. {
  1112. list.Add(new VirtualFolderInfo
  1113. {
  1114. Locations = new List<string> { customPath },
  1115. Name = "Recorded Series",
  1116. CollectionType = CollectionType.TvShows
  1117. });
  1118. }
  1119. return list;
  1120. }
  1121. class ActiveRecordingInfo
  1122. {
  1123. public string Path { get; set; }
  1124. public string TimerId { get; set; }
  1125. public CancellationTokenSource CancellationTokenSource { get; set; }
  1126. }
  1127. }
  1128. }