EmbyTV.cs 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  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.Extensions;
  30. using MediaBrowser.Controller.Power;
  31. using Microsoft.Win32;
  32. namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
  33. {
  34. public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable
  35. {
  36. private readonly IApplicationHost _appHpst;
  37. private readonly ILogger _logger;
  38. private readonly IHttpClient _httpClient;
  39. private readonly IServerConfigurationManager _config;
  40. private readonly IJsonSerializer _jsonSerializer;
  41. private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
  42. private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
  43. private readonly TimerManager _timerProvider;
  44. private readonly LiveTvManager _liveTvManager;
  45. private readonly IFileSystem _fileSystem;
  46. private readonly ISecurityManager _security;
  47. private readonly ILibraryMonitor _libraryMonitor;
  48. private readonly ILibraryManager _libraryManager;
  49. private readonly IProviderManager _providerManager;
  50. private readonly IFileOrganizationService _organizationService;
  51. private readonly IMediaEncoder _mediaEncoder;
  52. public static EmbyTV Current;
  53. 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)
  54. {
  55. Current = this;
  56. _appHpst = appHost;
  57. _logger = logger;
  58. _httpClient = httpClient;
  59. _config = config;
  60. _fileSystem = fileSystem;
  61. _security = security;
  62. _libraryManager = libraryManager;
  63. _libraryMonitor = libraryMonitor;
  64. _providerManager = providerManager;
  65. _organizationService = organizationService;
  66. _mediaEncoder = mediaEncoder;
  67. _liveTvManager = (LiveTvManager)liveTvManager;
  68. _jsonSerializer = jsonSerializer;
  69. _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
  70. _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
  71. _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
  72. _timerProvider.TimerFired += _timerProvider_TimerFired;
  73. }
  74. public void Start()
  75. {
  76. _timerProvider.RestartTimers();
  77. SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
  78. }
  79. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  80. {
  81. _logger.Info("Power mode changed to {0}", e.Mode);
  82. if (e.Mode == PowerModes.Resume)
  83. {
  84. _timerProvider.RestartTimers();
  85. }
  86. }
  87. public event EventHandler DataSourceChanged;
  88. public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
  89. private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
  90. new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
  91. public string Name
  92. {
  93. get { return "Emby"; }
  94. }
  95. public string DataPath
  96. {
  97. get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
  98. }
  99. public string HomePageUrl
  100. {
  101. get { return "http://emby.media"; }
  102. }
  103. public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
  104. {
  105. var status = new LiveTvServiceStatusInfo();
  106. var list = new List<LiveTvTunerInfo>();
  107. foreach (var hostInstance in _liveTvManager.TunerHosts)
  108. {
  109. try
  110. {
  111. var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
  112. list.AddRange(tuners);
  113. }
  114. catch (Exception ex)
  115. {
  116. _logger.ErrorException("Error getting tuners", ex);
  117. }
  118. }
  119. status.Tuners = list;
  120. status.Status = LiveTvServiceStatus.Ok;
  121. status.Version = _appHpst.ApplicationVersion.ToString();
  122. status.IsVisible = false;
  123. return status;
  124. }
  125. public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
  126. {
  127. var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
  128. List<ChannelInfo> channels = null;
  129. foreach (var timer in seriesTimers)
  130. {
  131. List<ProgramInfo> epgData;
  132. if (timer.RecordAnyChannel)
  133. {
  134. if (channels == null)
  135. {
  136. channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList();
  137. }
  138. var channelIds = channels.Select(i => i.Id).ToList();
  139. epgData = GetEpgDataForChannels(channelIds);
  140. }
  141. else
  142. {
  143. epgData = GetEpgDataForChannel(timer.ChannelId);
  144. }
  145. await UpdateTimersForSeriesTimer(epgData, timer, true).ConfigureAwait(false);
  146. }
  147. var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
  148. foreach (var timer in timers.ToList())
  149. {
  150. if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
  151. {
  152. _timerProvider.Delete(timer);
  153. }
  154. }
  155. }
  156. private List<ChannelInfo> _channelCache = null;
  157. private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
  158. {
  159. if (enableCache && _channelCache != null)
  160. {
  161. return _channelCache.ToList();
  162. }
  163. var list = new List<ChannelInfo>();
  164. foreach (var hostInstance in _liveTvManager.TunerHosts)
  165. {
  166. try
  167. {
  168. var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
  169. list.AddRange(channels);
  170. }
  171. catch (Exception ex)
  172. {
  173. _logger.ErrorException("Error getting channels", ex);
  174. }
  175. }
  176. foreach (var provider in GetListingProviders())
  177. {
  178. var enabledChannels = list
  179. .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
  180. .ToList();
  181. if (enabledChannels.Count > 0)
  182. {
  183. try
  184. {
  185. await provider.Item1.AddMetadata(provider.Item2, enabledChannels, cancellationToken).ConfigureAwait(false);
  186. }
  187. catch (NotSupportedException)
  188. {
  189. }
  190. catch (Exception ex)
  191. {
  192. _logger.ErrorException("Error adding metadata", ex);
  193. }
  194. }
  195. }
  196. _channelCache = list.ToList();
  197. return list;
  198. }
  199. public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
  200. {
  201. return GetChannelsAsync(false, cancellationToken);
  202. }
  203. public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
  204. {
  205. var timers = _timerProvider
  206. .GetAll()
  207. .Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
  208. .ToList();
  209. foreach (var timer in timers)
  210. {
  211. CancelTimerInternal(timer.Id);
  212. }
  213. var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  214. if (remove != null)
  215. {
  216. _seriesTimerProvider.Delete(remove);
  217. }
  218. return Task.FromResult(true);
  219. }
  220. private void CancelTimerInternal(string timerId)
  221. {
  222. var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  223. if (remove != null)
  224. {
  225. _timerProvider.Delete(remove);
  226. }
  227. ActiveRecordingInfo activeRecordingInfo;
  228. if (_activeRecordings.TryGetValue(timerId, out activeRecordingInfo))
  229. {
  230. activeRecordingInfo.CancellationTokenSource.Cancel();
  231. }
  232. }
  233. public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
  234. {
  235. CancelTimerInternal(timerId);
  236. return Task.FromResult(true);
  237. }
  238. public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
  239. {
  240. var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
  241. if (remove != null)
  242. {
  243. if (!string.IsNullOrWhiteSpace(remove.TimerId))
  244. {
  245. var enableDelay = _activeRecordings.ContainsKey(remove.TimerId);
  246. CancelTimerInternal(remove.TimerId);
  247. if (enableDelay)
  248. {
  249. // A hack yes, but need to make sure the file is closed before attempting to delete it
  250. await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  251. }
  252. }
  253. if (!string.IsNullOrWhiteSpace(remove.Path))
  254. {
  255. try
  256. {
  257. _fileSystem.DeleteFile(remove.Path);
  258. }
  259. catch (DirectoryNotFoundException)
  260. {
  261. }
  262. catch (FileNotFoundException)
  263. {
  264. }
  265. catch (Exception ex)
  266. {
  267. _logger.ErrorException("Error deleting recording file {0}", ex, remove.Path);
  268. }
  269. }
  270. _recordingProvider.Delete(remove);
  271. }
  272. else
  273. {
  274. throw new ResourceNotFoundException("Recording not found: " + recordingId);
  275. }
  276. }
  277. public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  278. {
  279. info.Id = Guid.NewGuid().ToString("N");
  280. _timerProvider.Add(info);
  281. return Task.FromResult(0);
  282. }
  283. public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  284. {
  285. info.Id = Guid.NewGuid().ToString("N");
  286. List<ProgramInfo> epgData;
  287. if (info.RecordAnyChannel)
  288. {
  289. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  290. var channelIds = channels.Select(i => i.Id).ToList();
  291. epgData = GetEpgDataForChannels(channelIds);
  292. }
  293. else
  294. {
  295. epgData = GetEpgDataForChannel(info.ChannelId);
  296. }
  297. // populate info.seriesID
  298. var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
  299. if (program != null)
  300. {
  301. info.SeriesId = program.SeriesId;
  302. }
  303. else
  304. {
  305. throw new InvalidOperationException("SeriesId for program not found");
  306. }
  307. _seriesTimerProvider.Add(info);
  308. await UpdateTimersForSeriesTimer(epgData, info, false).ConfigureAwait(false);
  309. }
  310. public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  311. {
  312. var instance = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
  313. if (instance != null)
  314. {
  315. instance.ChannelId = info.ChannelId;
  316. instance.Days = info.Days;
  317. instance.EndDate = info.EndDate;
  318. instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
  319. instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
  320. instance.PostPaddingSeconds = info.PostPaddingSeconds;
  321. instance.PrePaddingSeconds = info.PrePaddingSeconds;
  322. instance.Priority = info.Priority;
  323. instance.RecordAnyChannel = info.RecordAnyChannel;
  324. instance.RecordAnyTime = info.RecordAnyTime;
  325. instance.RecordNewOnly = info.RecordNewOnly;
  326. instance.StartDate = info.StartDate;
  327. _seriesTimerProvider.Update(instance);
  328. List<ProgramInfo> epgData;
  329. if (instance.RecordAnyChannel)
  330. {
  331. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  332. var channelIds = channels.Select(i => i.Id).ToList();
  333. epgData = GetEpgDataForChannels(channelIds);
  334. }
  335. else
  336. {
  337. epgData = GetEpgDataForChannel(instance.ChannelId);
  338. }
  339. await UpdateTimersForSeriesTimer(epgData, instance, true).ConfigureAwait(false);
  340. }
  341. }
  342. public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  343. {
  344. _timerProvider.Update(info);
  345. return Task.FromResult(true);
  346. }
  347. public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
  348. {
  349. throw new NotImplementedException();
  350. }
  351. public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
  352. {
  353. throw new NotImplementedException();
  354. }
  355. public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
  356. {
  357. throw new NotImplementedException();
  358. }
  359. public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
  360. {
  361. var recordings = _recordingProvider.GetAll().ToList();
  362. var updated = false;
  363. foreach (var recording in recordings)
  364. {
  365. if (recording.Status == RecordingStatus.InProgress)
  366. {
  367. if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId))
  368. {
  369. recording.Status = RecordingStatus.Cancelled;
  370. recording.DateLastUpdated = DateTime.UtcNow;
  371. _recordingProvider.Update(recording);
  372. updated = true;
  373. }
  374. }
  375. }
  376. if (updated)
  377. {
  378. recordings = _recordingProvider.GetAll().ToList();
  379. }
  380. return recordings;
  381. }
  382. public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
  383. {
  384. return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
  385. }
  386. public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
  387. {
  388. var config = GetConfiguration();
  389. var defaults = new SeriesTimerInfo()
  390. {
  391. PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
  392. PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
  393. RecordAnyChannel = false,
  394. RecordAnyTime = false,
  395. RecordNewOnly = false
  396. };
  397. if (program != null)
  398. {
  399. defaults.SeriesId = program.SeriesId;
  400. defaults.ProgramId = program.Id;
  401. }
  402. return Task.FromResult(defaults);
  403. }
  404. public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
  405. {
  406. return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
  407. }
  408. public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  409. {
  410. try
  411. {
  412. return await GetProgramsAsyncInternal(channelId, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false);
  413. }
  414. catch (OperationCanceledException)
  415. {
  416. throw;
  417. }
  418. catch (Exception ex)
  419. {
  420. _logger.ErrorException("Error getting programs", ex);
  421. return GetEpgDataForChannel(channelId).Where(i => i.StartDate <= endDateUtc && i.EndDate >= startDateUtc);
  422. }
  423. }
  424. private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
  425. {
  426. if (info.EnableAllTuners)
  427. {
  428. return true;
  429. }
  430. if (string.IsNullOrWhiteSpace(tunerHostId))
  431. {
  432. throw new ArgumentNullException("tunerHostId");
  433. }
  434. return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
  435. }
  436. private async Task<IEnumerable<ProgramInfo>> GetProgramsAsyncInternal(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  437. {
  438. var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
  439. var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  440. foreach (var provider in GetListingProviders())
  441. {
  442. if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
  443. {
  444. _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);
  445. continue;
  446. }
  447. _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
  448. var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken)
  449. .ConfigureAwait(false);
  450. var list = programs.ToList();
  451. // Replace the value that came from the provider with a normalized value
  452. foreach (var program in list)
  453. {
  454. program.ChannelId = channelId;
  455. }
  456. if (list.Count > 0)
  457. {
  458. SaveEpgDataForChannel(channelId, list);
  459. return list;
  460. }
  461. }
  462. return new List<ProgramInfo>();
  463. }
  464. private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
  465. {
  466. return GetConfiguration().ListingProviders
  467. .Select(i =>
  468. {
  469. var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
  470. return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
  471. })
  472. .Where(i => i != null)
  473. .ToList();
  474. }
  475. public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
  476. {
  477. throw new NotImplementedException();
  478. }
  479. public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
  480. {
  481. _logger.Info("Streaming Channel " + channelId);
  482. foreach (var hostInstance in _liveTvManager.TunerHosts)
  483. {
  484. try
  485. {
  486. var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  487. result.Item2.Release();
  488. return result.Item1;
  489. }
  490. catch (Exception e)
  491. {
  492. _logger.ErrorException("Error getting channel stream", e);
  493. }
  494. }
  495. throw new ApplicationException("Tuner not found.");
  496. }
  497. private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
  498. {
  499. _logger.Info("Streaming Channel " + channelId);
  500. foreach (var hostInstance in _liveTvManager.TunerHosts)
  501. {
  502. try
  503. {
  504. var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  505. return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2);
  506. }
  507. catch (Exception e)
  508. {
  509. _logger.ErrorException("Error getting channel stream", e);
  510. }
  511. }
  512. throw new ApplicationException("Tuner not found.");
  513. }
  514. public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
  515. {
  516. foreach (var hostInstance in _liveTvManager.TunerHosts)
  517. {
  518. try
  519. {
  520. var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
  521. if (sources.Count > 0)
  522. {
  523. return sources;
  524. }
  525. }
  526. catch (NotImplementedException)
  527. {
  528. }
  529. }
  530. throw new NotImplementedException();
  531. }
  532. public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
  533. {
  534. throw new NotImplementedException();
  535. }
  536. public Task CloseLiveStream(string id, CancellationToken cancellationToken)
  537. {
  538. return Task.FromResult(0);
  539. }
  540. public Task RecordLiveStream(string id, CancellationToken cancellationToken)
  541. {
  542. return Task.FromResult(0);
  543. }
  544. public Task ResetTuner(string id, CancellationToken cancellationToken)
  545. {
  546. return Task.FromResult(0);
  547. }
  548. async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
  549. {
  550. var timer = e.Argument;
  551. _logger.Info("Recording timer fired.");
  552. try
  553. {
  554. var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
  555. if (recordingEndDate <= DateTime.UtcNow)
  556. {
  557. _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
  558. return;
  559. }
  560. var activeRecordingInfo = new ActiveRecordingInfo
  561. {
  562. CancellationTokenSource = new CancellationTokenSource(),
  563. TimerId = timer.Id
  564. };
  565. if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
  566. {
  567. await RecordStream(timer, recordingEndDate, activeRecordingInfo, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
  568. }
  569. else
  570. {
  571. _logger.Info("Skipping RecordStream because it's already in progress.");
  572. }
  573. }
  574. catch (OperationCanceledException)
  575. {
  576. }
  577. catch (Exception ex)
  578. {
  579. _logger.ErrorException("Error recording stream", ex);
  580. }
  581. }
  582. private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
  583. {
  584. if (timer == null)
  585. {
  586. throw new ArgumentNullException("timer");
  587. }
  588. ProgramInfo info = null;
  589. if (string.IsNullOrWhiteSpace(timer.ProgramId))
  590. {
  591. _logger.Info("Timer {0} has null programId", timer.Id);
  592. }
  593. else
  594. {
  595. info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  596. }
  597. if (info == null)
  598. {
  599. _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
  600. info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
  601. }
  602. if (info == null)
  603. {
  604. throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
  605. }
  606. var recordPath = RecordingPath;
  607. if (info.IsMovie)
  608. {
  609. recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim());
  610. }
  611. else if (info.IsSeries)
  612. {
  613. recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim());
  614. if (info.SeasonNumber.HasValue)
  615. {
  616. var folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
  617. recordPath = Path.Combine(recordPath, folderName);
  618. }
  619. }
  620. else if (info.IsKids)
  621. {
  622. recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim());
  623. }
  624. else if (info.IsSports)
  625. {
  626. recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim());
  627. }
  628. else
  629. {
  630. recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim());
  631. }
  632. var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
  633. recordPath = Path.Combine(recordPath, recordingFileName);
  634. var recordingId = info.Id.GetMD5().ToString("N");
  635. var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase));
  636. if (recording == null)
  637. {
  638. recording = new RecordingInfo
  639. {
  640. ChannelId = info.ChannelId,
  641. Id = recordingId,
  642. StartDate = info.StartDate,
  643. EndDate = info.EndDate,
  644. Genres = info.Genres,
  645. IsKids = info.IsKids,
  646. IsLive = info.IsLive,
  647. IsMovie = info.IsMovie,
  648. IsHD = info.IsHD,
  649. IsNews = info.IsNews,
  650. IsPremiere = info.IsPremiere,
  651. IsSeries = info.IsSeries,
  652. IsSports = info.IsSports,
  653. IsRepeat = !info.IsPremiere,
  654. Name = info.Name,
  655. EpisodeTitle = info.EpisodeTitle,
  656. ProgramId = info.Id,
  657. ImagePath = info.ImagePath,
  658. ImageUrl = info.ImageUrl,
  659. OriginalAirDate = info.OriginalAirDate,
  660. Status = RecordingStatus.Scheduled,
  661. Overview = info.Overview,
  662. SeriesTimerId = timer.SeriesTimerId,
  663. TimerId = timer.Id,
  664. ShowId = info.ShowId
  665. };
  666. _recordingProvider.AddOrUpdate(recording);
  667. }
  668. try
  669. {
  670. var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
  671. var mediaStreamInfo = result.Item1;
  672. var isResourceOpen = true;
  673. // Unfortunately due to the semaphore we have to have a nested try/finally
  674. try
  675. {
  676. // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
  677. //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  678. var recorder = await GetRecorder().ConfigureAwait(false);
  679. if (recorder is EncodedRecorder)
  680. {
  681. recordPath = Path.ChangeExtension(recordPath, ".mp4");
  682. }
  683. recordPath = EnsureFileUnique(recordPath, timer.Id);
  684. _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
  685. activeRecordingInfo.Path = recordPath;
  686. _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
  687. recording.Path = recordPath;
  688. recording.Status = RecordingStatus.InProgress;
  689. recording.DateLastUpdated = DateTime.UtcNow;
  690. _recordingProvider.AddOrUpdate(recording);
  691. var duration = recordingEndDate - DateTime.UtcNow;
  692. _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  693. _logger.Info("Writing file to path: " + recordPath);
  694. _logger.Info("Opening recording stream from tuner provider");
  695. Action onStarted = () =>
  696. {
  697. result.Item3.Release();
  698. isResourceOpen = false;
  699. };
  700. var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration);
  701. // If it supports supplying duration via url
  702. if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
  703. {
  704. mediaStreamInfo.Path = pathWithDuration;
  705. mediaStreamInfo.RunTimeTicks = duration.Ticks;
  706. }
  707. await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
  708. recording.Status = RecordingStatus.Completed;
  709. _logger.Info("Recording completed: {0}", recordPath);
  710. }
  711. finally
  712. {
  713. if (isResourceOpen)
  714. {
  715. result.Item3.Release();
  716. }
  717. _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false);
  718. }
  719. }
  720. catch (OperationCanceledException)
  721. {
  722. _logger.Info("Recording stopped: {0}", recordPath);
  723. recording.Status = RecordingStatus.Completed;
  724. }
  725. catch (Exception ex)
  726. {
  727. _logger.ErrorException("Error recording to {0}", ex, recordPath);
  728. recording.Status = RecordingStatus.Error;
  729. }
  730. finally
  731. {
  732. ActiveRecordingInfo removed;
  733. _activeRecordings.TryRemove(timer.Id, out removed);
  734. }
  735. recording.DateLastUpdated = DateTime.UtcNow;
  736. _recordingProvider.AddOrUpdate(recording);
  737. if (recording.Status == RecordingStatus.Completed)
  738. {
  739. OnSuccessfulRecording(recording);
  740. _timerProvider.Delete(timer);
  741. }
  742. else if (DateTime.UtcNow < timer.EndDate)
  743. {
  744. const int retryIntervalSeconds = 60;
  745. _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
  746. _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds));
  747. }
  748. else
  749. {
  750. _timerProvider.Delete(timer);
  751. _recordingProvider.Delete(recording);
  752. }
  753. }
  754. private string EnsureFileUnique(string path, string timerId)
  755. {
  756. var originalPath = path;
  757. var index = 1;
  758. while (FileExists(path, timerId))
  759. {
  760. var parent = Path.GetDirectoryName(originalPath);
  761. var name = Path.GetFileNameWithoutExtension(originalPath);
  762. name += "-" + index.ToString(CultureInfo.InvariantCulture);
  763. path = Path.ChangeExtension(Path.Combine(parent, name), Path.GetExtension(originalPath));
  764. index++;
  765. }
  766. return path;
  767. }
  768. private bool FileExists(string path, string timerId)
  769. {
  770. if (_fileSystem.FileExists(path))
  771. {
  772. return true;
  773. }
  774. var hasRecordingAtPath = _activeRecordings.Values.ToList().Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.TimerId, timerId, StringComparison.OrdinalIgnoreCase));
  775. if (hasRecordingAtPath)
  776. {
  777. return true;
  778. }
  779. return false;
  780. }
  781. private async Task<IRecorder> GetRecorder()
  782. {
  783. var config = GetConfiguration();
  784. if (config.EnableRecordingEncoding)
  785. {
  786. var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false);
  787. if (regInfo.IsValid)
  788. {
  789. return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config);
  790. }
  791. }
  792. return new DirectRecorder(_logger, _httpClient, _fileSystem);
  793. }
  794. private async void OnSuccessfulRecording(RecordingInfo recording)
  795. {
  796. if (GetConfiguration().EnableAutoOrganize)
  797. {
  798. if (recording.IsSeries)
  799. {
  800. try
  801. {
  802. // this is to account for the library monitor holding a lock for additional time after the change is complete.
  803. // ideally this shouldn't be hard-coded
  804. await Task.Delay(30000).ConfigureAwait(false);
  805. var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
  806. var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false);
  807. if (result.Status == FileSortingStatus.Success)
  808. {
  809. _recordingProvider.Delete(recording);
  810. }
  811. }
  812. catch (Exception ex)
  813. {
  814. _logger.ErrorException("Error processing new recording", ex);
  815. }
  816. }
  817. }
  818. }
  819. private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
  820. {
  821. var epgData = GetEpgDataForChannel(channelId);
  822. return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
  823. }
  824. private ProgramInfo GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
  825. {
  826. var epgData = GetEpgDataForChannel(channelId);
  827. var startDateTicks = startDateUtc.Ticks;
  828. // Find the first program that starts within 3 minutes
  829. return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
  830. }
  831. private string RecordingPath
  832. {
  833. get
  834. {
  835. var path = GetConfiguration().RecordingPath;
  836. return string.IsNullOrWhiteSpace(path)
  837. ? Path.Combine(DataPath, "recordings")
  838. : path;
  839. }
  840. }
  841. private LiveTvOptions GetConfiguration()
  842. {
  843. return _config.GetConfiguration<LiveTvOptions>("livetv");
  844. }
  845. private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
  846. {
  847. var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
  848. var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
  849. if (registration.IsValid)
  850. {
  851. foreach (var timer in newTimers)
  852. {
  853. _timerProvider.AddOrUpdate(timer);
  854. }
  855. }
  856. if (deleteInvalidTimers)
  857. {
  858. var allTimers = GetTimersForSeries(seriesTimer, epgData, new List<RecordingInfo>())
  859. .Select(i => i.Id)
  860. .ToList();
  861. var deletes = _timerProvider.GetAll()
  862. .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
  863. .Where(i => !allTimers.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
  864. .ToList();
  865. foreach (var timer in deletes)
  866. {
  867. await CancelTimerAsync(timer.Id, CancellationToken.None).ConfigureAwait(false);
  868. }
  869. }
  870. }
  871. private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings)
  872. {
  873. if (seriesTimer == null)
  874. {
  875. throw new ArgumentNullException("seriesTimer");
  876. }
  877. if (allPrograms == null)
  878. {
  879. throw new ArgumentNullException("allPrograms");
  880. }
  881. if (currentRecordings == null)
  882. {
  883. throw new ArgumentNullException("currentRecordings");
  884. }
  885. // Exclude programs that have already ended
  886. allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
  887. allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
  888. var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
  889. allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase));
  890. return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
  891. }
  892. private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
  893. {
  894. if (!seriesTimer.RecordAnyTime)
  895. {
  896. allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks);
  897. }
  898. if (seriesTimer.RecordNewOnly)
  899. {
  900. allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
  901. }
  902. if (!seriesTimer.RecordAnyChannel)
  903. {
  904. allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
  905. }
  906. allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
  907. if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
  908. {
  909. _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
  910. return new List<ProgramInfo>();
  911. }
  912. return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
  913. }
  914. private string GetChannelEpgCachePath(string channelId)
  915. {
  916. return Path.Combine(_config.CommonApplicationPaths.CachePath, "embytvepg", channelId + ".json");
  917. }
  918. private readonly object _epgLock = new object();
  919. private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
  920. {
  921. var path = GetChannelEpgCachePath(channelId);
  922. _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
  923. lock (_epgLock)
  924. {
  925. _jsonSerializer.SerializeToFile(epgData, path);
  926. }
  927. }
  928. private List<ProgramInfo> GetEpgDataForChannel(string channelId)
  929. {
  930. try
  931. {
  932. lock (_epgLock)
  933. {
  934. return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
  935. }
  936. }
  937. catch
  938. {
  939. return new List<ProgramInfo>();
  940. }
  941. }
  942. private List<ProgramInfo> GetEpgDataForChannels(List<string> channelIds)
  943. {
  944. return channelIds.SelectMany(GetEpgDataForChannel).ToList();
  945. }
  946. public void Dispose()
  947. {
  948. foreach (var pair in _activeRecordings.ToList())
  949. {
  950. pair.Value.CancellationTokenSource.Cancel();
  951. }
  952. }
  953. public Task<MBRegistrationRecord> GetRegistrationInfo(string feature)
  954. {
  955. if (string.Equals(feature, "seriesrecordings", StringComparison.OrdinalIgnoreCase))
  956. {
  957. return _security.GetRegistrationStatus("embytvseriesrecordings");
  958. }
  959. return Task.FromResult(new MBRegistrationRecord
  960. {
  961. IsValid = true,
  962. IsRegistered = true
  963. });
  964. }
  965. class ActiveRecordingInfo
  966. {
  967. public string Path { get; set; }
  968. public string TimerId { get; set; }
  969. public CancellationTokenSource CancellationTokenSource { get; set; }
  970. }
  971. }
  972. }