EmbyTV.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. using MediaBrowser.Common;
  2. using MediaBrowser.Common.Configuration;
  3. using MediaBrowser.Common.IO;
  4. using MediaBrowser.Common.Net;
  5. using MediaBrowser.Common.Security;
  6. using MediaBrowser.Controller.Configuration;
  7. using MediaBrowser.Controller.Drawing;
  8. using MediaBrowser.Controller.FileOrganization;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Controller.LiveTv;
  11. using MediaBrowser.Controller.MediaEncoding;
  12. using MediaBrowser.Controller.Providers;
  13. using MediaBrowser.Model.Dlna;
  14. using MediaBrowser.Model.Dto;
  15. using MediaBrowser.Model.Entities;
  16. using MediaBrowser.Model.Events;
  17. using MediaBrowser.Model.FileOrganization;
  18. using MediaBrowser.Model.LiveTv;
  19. using MediaBrowser.Model.Logging;
  20. using MediaBrowser.Model.Serialization;
  21. using MediaBrowser.Server.Implementations.FileOrganization;
  22. using System;
  23. using System.Collections.Concurrent;
  24. using System.Collections.Generic;
  25. using System.IO;
  26. using System.Linq;
  27. using System.Threading;
  28. using System.Threading.Tasks;
  29. using CommonIO;
  30. using MediaBrowser.Common.Extensions;
  31. namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
  32. {
  33. public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable
  34. {
  35. private readonly IApplicationHost _appHpst;
  36. private readonly ILogger _logger;
  37. private readonly IHttpClient _httpClient;
  38. private readonly IServerConfigurationManager _config;
  39. private readonly IJsonSerializer _jsonSerializer;
  40. private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
  41. private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
  42. private readonly TimerManager _timerProvider;
  43. private readonly LiveTvManager _liveTvManager;
  44. private readonly IFileSystem _fileSystem;
  45. private readonly ISecurityManager _security;
  46. private readonly ILibraryMonitor _libraryMonitor;
  47. private readonly ILibraryManager _libraryManager;
  48. private readonly IProviderManager _providerManager;
  49. private readonly IFileOrganizationService _organizationService;
  50. private readonly IMediaEncoder _mediaEncoder;
  51. public static EmbyTV Current;
  52. 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)
  53. {
  54. Current = this;
  55. _appHpst = appHost;
  56. _logger = logger;
  57. _httpClient = httpClient;
  58. _config = config;
  59. _fileSystem = fileSystem;
  60. _security = security;
  61. _libraryManager = libraryManager;
  62. _libraryMonitor = libraryMonitor;
  63. _providerManager = providerManager;
  64. _organizationService = organizationService;
  65. _mediaEncoder = mediaEncoder;
  66. _liveTvManager = (LiveTvManager)liveTvManager;
  67. _jsonSerializer = jsonSerializer;
  68. _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
  69. _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
  70. _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"));
  71. _timerProvider.TimerFired += _timerProvider_TimerFired;
  72. }
  73. public void Start()
  74. {
  75. _timerProvider.RestartTimers();
  76. }
  77. public event EventHandler DataSourceChanged;
  78. public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
  79. private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeRecordings =
  80. new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.OrdinalIgnoreCase);
  81. public string Name
  82. {
  83. get { return "Emby"; }
  84. }
  85. public string DataPath
  86. {
  87. get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
  88. }
  89. public string HomePageUrl
  90. {
  91. get { return "http://emby.media"; }
  92. }
  93. public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
  94. {
  95. var status = new LiveTvServiceStatusInfo();
  96. var list = new List<LiveTvTunerInfo>();
  97. foreach (var hostInstance in _liveTvManager.TunerHosts)
  98. {
  99. try
  100. {
  101. var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
  102. list.AddRange(tuners);
  103. }
  104. catch (Exception ex)
  105. {
  106. _logger.ErrorException("Error getting tuners", ex);
  107. }
  108. }
  109. status.Tuners = list;
  110. status.Status = LiveTvServiceStatus.Ok;
  111. status.Version = _appHpst.ApplicationVersion.ToString();
  112. status.IsVisible = false;
  113. return status;
  114. }
  115. public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
  116. {
  117. var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
  118. List<ChannelInfo> channels = null;
  119. foreach (var timer in seriesTimers)
  120. {
  121. List<ProgramInfo> epgData;
  122. if (timer.RecordAnyChannel)
  123. {
  124. if (channels == null)
  125. {
  126. channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList();
  127. }
  128. var channelIds = channels.Select(i => i.Id).ToList();
  129. epgData = GetEpgDataForChannels(channelIds);
  130. }
  131. else
  132. {
  133. epgData = GetEpgDataForChannel(timer.ChannelId);
  134. }
  135. await UpdateTimersForSeriesTimer(epgData, timer).ConfigureAwait(false);
  136. }
  137. var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
  138. foreach (var timer in timers.ToList())
  139. {
  140. if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
  141. {
  142. _timerProvider.Delete(timer);
  143. }
  144. }
  145. }
  146. private List<ChannelInfo> _channelCache = null;
  147. private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
  148. {
  149. if (enableCache && _channelCache != null)
  150. {
  151. return _channelCache.ToList();
  152. }
  153. var list = new List<ChannelInfo>();
  154. foreach (var hostInstance in _liveTvManager.TunerHosts)
  155. {
  156. try
  157. {
  158. var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
  159. list.AddRange(channels);
  160. }
  161. catch (Exception ex)
  162. {
  163. _logger.ErrorException("Error getting channels", ex);
  164. }
  165. }
  166. if (list.Count > 0)
  167. {
  168. foreach (var provider in GetListingProviders())
  169. {
  170. try
  171. {
  172. await provider.Item1.AddMetadata(provider.Item2, list, cancellationToken).ConfigureAwait(false);
  173. }
  174. catch (NotSupportedException)
  175. {
  176. }
  177. catch (Exception ex)
  178. {
  179. _logger.ErrorException("Error adding metadata", ex);
  180. }
  181. }
  182. }
  183. _channelCache = list;
  184. return list;
  185. }
  186. public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
  187. {
  188. return GetChannelsAsync(false, cancellationToken);
  189. }
  190. public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
  191. {
  192. var timers = _timerProvider.GetAll().Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase));
  193. foreach (var timer in timers)
  194. {
  195. CancelTimerInternal(timer.Id);
  196. }
  197. var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  198. if (remove != null)
  199. {
  200. _seriesTimerProvider.Delete(remove);
  201. }
  202. return Task.FromResult(true);
  203. }
  204. private void CancelTimerInternal(string timerId)
  205. {
  206. var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  207. if (remove != null)
  208. {
  209. _timerProvider.Delete(remove);
  210. }
  211. CancellationTokenSource cancellationTokenSource;
  212. if (_activeRecordings.TryGetValue(timerId, out cancellationTokenSource))
  213. {
  214. cancellationTokenSource.Cancel();
  215. }
  216. }
  217. public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
  218. {
  219. CancelTimerInternal(timerId);
  220. return Task.FromResult(true);
  221. }
  222. public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
  223. {
  224. var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
  225. if (remove != null)
  226. {
  227. if (!string.IsNullOrWhiteSpace(remove.TimerId))
  228. {
  229. var enableDelay = _activeRecordings.ContainsKey(remove.TimerId);
  230. CancelTimerInternal(remove.TimerId);
  231. if (enableDelay)
  232. {
  233. // A hack yes, but need to make sure the file is closed before attempting to delete it
  234. await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  235. }
  236. }
  237. try
  238. {
  239. _fileSystem.DeleteFile(remove.Path);
  240. }
  241. catch (DirectoryNotFoundException)
  242. {
  243. }
  244. catch (FileNotFoundException)
  245. {
  246. }
  247. _recordingProvider.Delete(remove);
  248. }
  249. else
  250. {
  251. throw new ResourceNotFoundException("Recording not found: " + recordingId);
  252. }
  253. }
  254. public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  255. {
  256. info.Id = Guid.NewGuid().ToString("N");
  257. _timerProvider.Add(info);
  258. return Task.FromResult(0);
  259. }
  260. public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  261. {
  262. info.Id = Guid.NewGuid().ToString("N");
  263. List<ProgramInfo> epgData;
  264. if (info.RecordAnyChannel)
  265. {
  266. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  267. var channelIds = channels.Select(i => i.Id).ToList();
  268. epgData = GetEpgDataForChannels(channelIds);
  269. }
  270. else
  271. {
  272. epgData = GetEpgDataForChannel(info.ChannelId);
  273. }
  274. // populate info.seriesID
  275. var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
  276. if (program != null)
  277. {
  278. info.SeriesId = program.SeriesId;
  279. }
  280. else
  281. {
  282. throw new InvalidOperationException("SeriesId for program not found");
  283. }
  284. _seriesTimerProvider.Add(info);
  285. await UpdateTimersForSeriesTimer(epgData, info).ConfigureAwait(false);
  286. }
  287. public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  288. {
  289. _seriesTimerProvider.Update(info);
  290. List<ProgramInfo> epgData;
  291. if (info.RecordAnyChannel)
  292. {
  293. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  294. var channelIds = channels.Select(i => i.Id).ToList();
  295. epgData = GetEpgDataForChannels(channelIds);
  296. }
  297. else
  298. {
  299. epgData = GetEpgDataForChannel(info.ChannelId);
  300. }
  301. await UpdateTimersForSeriesTimer(epgData, info).ConfigureAwait(false);
  302. }
  303. public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  304. {
  305. _timerProvider.Update(info);
  306. return Task.FromResult(true);
  307. }
  308. public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
  309. {
  310. throw new NotImplementedException();
  311. }
  312. public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
  313. {
  314. throw new NotImplementedException();
  315. }
  316. public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
  317. {
  318. throw new NotImplementedException();
  319. }
  320. public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
  321. {
  322. var recordings = _recordingProvider.GetAll().ToList();
  323. var updated = false;
  324. foreach (var recording in recordings)
  325. {
  326. if (recording.Status == RecordingStatus.InProgress)
  327. {
  328. if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId))
  329. {
  330. recording.Status = RecordingStatus.Cancelled;
  331. recording.DateLastUpdated = DateTime.UtcNow;
  332. _recordingProvider.Update(recording);
  333. updated = true;
  334. }
  335. }
  336. }
  337. if (updated)
  338. {
  339. recordings = _recordingProvider.GetAll().ToList();
  340. }
  341. return recordings;
  342. }
  343. public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
  344. {
  345. return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
  346. }
  347. public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
  348. {
  349. var config = GetConfiguration();
  350. var defaults = new SeriesTimerInfo()
  351. {
  352. PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
  353. PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
  354. RecordAnyChannel = false,
  355. RecordAnyTime = false,
  356. RecordNewOnly = false
  357. };
  358. if (program != null)
  359. {
  360. defaults.SeriesId = program.SeriesId;
  361. defaults.ProgramId = program.Id;
  362. }
  363. return Task.FromResult(defaults);
  364. }
  365. public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
  366. {
  367. return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
  368. }
  369. public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  370. {
  371. try
  372. {
  373. return await GetProgramsAsyncInternal(channelId, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false);
  374. }
  375. catch (OperationCanceledException)
  376. {
  377. throw;
  378. }
  379. catch (Exception ex)
  380. {
  381. _logger.ErrorException("Error getting programs", ex);
  382. return GetEpgDataForChannel(channelId).Where(i => i.StartDate <= endDateUtc && i.EndDate >= startDateUtc);
  383. }
  384. }
  385. private async Task<IEnumerable<ProgramInfo>> GetProgramsAsyncInternal(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  386. {
  387. var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
  388. var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  389. foreach (var provider in GetListingProviders())
  390. {
  391. var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken)
  392. .ConfigureAwait(false);
  393. var list = programs.ToList();
  394. // Replace the value that came from the provider with a normalized value
  395. foreach (var program in list)
  396. {
  397. program.ChannelId = channelId;
  398. }
  399. if (list.Count > 0)
  400. {
  401. SaveEpgDataForChannel(channelId, list);
  402. return list;
  403. }
  404. }
  405. return new List<ProgramInfo>();
  406. }
  407. private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
  408. {
  409. return GetConfiguration().ListingProviders
  410. .Select(i =>
  411. {
  412. var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
  413. return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
  414. })
  415. .Where(i => i != null)
  416. .ToList();
  417. }
  418. public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
  419. {
  420. throw new NotImplementedException();
  421. }
  422. public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
  423. {
  424. _logger.Info("Streaming Channel " + channelId);
  425. foreach (var hostInstance in _liveTvManager.TunerHosts)
  426. {
  427. try
  428. {
  429. var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  430. result.Item2.Release();
  431. return result.Item1;
  432. }
  433. catch (Exception e)
  434. {
  435. _logger.ErrorException("Error getting channel stream", e);
  436. }
  437. }
  438. throw new ApplicationException("Tuner not found.");
  439. }
  440. private async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
  441. {
  442. _logger.Info("Streaming Channel " + channelId);
  443. foreach (var hostInstance in _liveTvManager.TunerHosts)
  444. {
  445. try
  446. {
  447. return await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  448. }
  449. catch (Exception e)
  450. {
  451. _logger.ErrorException("Error getting channel stream", e);
  452. }
  453. }
  454. throw new ApplicationException("Tuner not found.");
  455. }
  456. public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
  457. {
  458. foreach (var hostInstance in _liveTvManager.TunerHosts)
  459. {
  460. try
  461. {
  462. var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
  463. if (sources.Count > 0)
  464. {
  465. return sources;
  466. }
  467. }
  468. catch (NotImplementedException)
  469. {
  470. }
  471. }
  472. throw new NotImplementedException();
  473. }
  474. public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
  475. {
  476. throw new NotImplementedException();
  477. }
  478. public Task CloseLiveStream(string id, CancellationToken cancellationToken)
  479. {
  480. return Task.FromResult(0);
  481. }
  482. public Task RecordLiveStream(string id, CancellationToken cancellationToken)
  483. {
  484. return Task.FromResult(0);
  485. }
  486. public Task ResetTuner(string id, CancellationToken cancellationToken)
  487. {
  488. return Task.FromResult(0);
  489. }
  490. async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
  491. {
  492. var timer = e.Argument;
  493. _logger.Info("Recording timer fired.");
  494. try
  495. {
  496. var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
  497. if (recordingEndDate <= DateTime.UtcNow)
  498. {
  499. _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
  500. return;
  501. }
  502. var cancellationTokenSource = new CancellationTokenSource();
  503. if (_activeRecordings.TryAdd(timer.Id, cancellationTokenSource))
  504. {
  505. await RecordStream(timer, recordingEndDate, cancellationTokenSource.Token).ConfigureAwait(false);
  506. }
  507. }
  508. catch (OperationCanceledException)
  509. {
  510. }
  511. catch (Exception ex)
  512. {
  513. _logger.ErrorException("Error recording stream", ex);
  514. }
  515. }
  516. private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, CancellationToken cancellationToken)
  517. {
  518. if (timer == null)
  519. {
  520. throw new ArgumentNullException("timer");
  521. }
  522. if (string.IsNullOrWhiteSpace(timer.ProgramId))
  523. {
  524. throw new InvalidOperationException("timer.ProgramId is null. Cannot record.");
  525. }
  526. var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  527. if (info == null)
  528. {
  529. throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
  530. }
  531. var recordPath = RecordingPath;
  532. if (info.IsMovie)
  533. {
  534. recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim());
  535. }
  536. else if (info.IsSeries)
  537. {
  538. recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim());
  539. }
  540. else if (info.IsKids)
  541. {
  542. recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim());
  543. }
  544. else if (info.IsSports)
  545. {
  546. recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim());
  547. }
  548. else
  549. {
  550. recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim());
  551. }
  552. var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
  553. recordPath = Path.Combine(recordPath, recordingFileName);
  554. _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
  555. var recordingId = info.Id.GetMD5().ToString("N");
  556. var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase));
  557. if (recording == null)
  558. {
  559. recording = new RecordingInfo
  560. {
  561. ChannelId = info.ChannelId,
  562. Id = recordingId,
  563. StartDate = info.StartDate,
  564. EndDate = info.EndDate,
  565. Genres = info.Genres,
  566. IsKids = info.IsKids,
  567. IsLive = info.IsLive,
  568. IsMovie = info.IsMovie,
  569. IsHD = info.IsHD,
  570. IsNews = info.IsNews,
  571. IsPremiere = info.IsPremiere,
  572. IsSeries = info.IsSeries,
  573. IsSports = info.IsSports,
  574. IsRepeat = !info.IsPremiere,
  575. Name = info.Name,
  576. EpisodeTitle = info.EpisodeTitle,
  577. ProgramId = info.Id,
  578. ImagePath = info.ImagePath,
  579. ImageUrl = info.ImageUrl,
  580. OriginalAirDate = info.OriginalAirDate,
  581. Status = RecordingStatus.Scheduled,
  582. Overview = info.Overview,
  583. SeriesTimerId = timer.SeriesTimerId,
  584. TimerId = timer.Id,
  585. ShowId = info.ShowId
  586. };
  587. _recordingProvider.AddOrUpdate(recording);
  588. }
  589. try
  590. {
  591. var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None);
  592. var mediaStreamInfo = result.Item1;
  593. var isResourceOpen = true;
  594. // Unfortunately due to the semaphore we have to have a nested try/finally
  595. try
  596. {
  597. // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
  598. await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  599. var duration = recordingEndDate - DateTime.UtcNow;
  600. HttpRequestOptions httpRequestOptions = new HttpRequestOptions()
  601. {
  602. Url = mediaStreamInfo.Path
  603. };
  604. recording.Path = recordPath;
  605. recording.Status = RecordingStatus.InProgress;
  606. recording.DateLastUpdated = DateTime.UtcNow;
  607. _recordingProvider.AddOrUpdate(recording);
  608. _logger.Info("Beginning recording.");
  609. httpRequestOptions.BufferContent = false;
  610. var durationToken = new CancellationTokenSource(duration);
  611. var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
  612. httpRequestOptions.CancellationToken = linkedToken;
  613. _logger.Info("Writing file to path: " + recordPath);
  614. using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET"))
  615. {
  616. using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read))
  617. {
  618. result.Item2.Release();
  619. isResourceOpen = false;
  620. await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken);
  621. }
  622. }
  623. recording.Status = RecordingStatus.Completed;
  624. _logger.Info("Recording completed");
  625. }
  626. finally
  627. {
  628. if (isResourceOpen)
  629. {
  630. result.Item2.Release();
  631. }
  632. }
  633. }
  634. catch (OperationCanceledException)
  635. {
  636. _logger.Info("Recording stopped");
  637. recording.Status = RecordingStatus.Completed;
  638. }
  639. catch (Exception ex)
  640. {
  641. _logger.ErrorException("Error recording", ex);
  642. recording.Status = RecordingStatus.Error;
  643. }
  644. finally
  645. {
  646. CancellationTokenSource removed;
  647. _activeRecordings.TryRemove(timer.Id, out removed);
  648. }
  649. recording.DateLastUpdated = DateTime.UtcNow;
  650. _recordingProvider.AddOrUpdate(recording);
  651. if (recording.Status == RecordingStatus.Completed)
  652. {
  653. OnSuccessfulRecording(recording);
  654. _timerProvider.Delete(timer);
  655. }
  656. else if (DateTime.UtcNow < timer.EndDate)
  657. {
  658. const int retryIntervalSeconds = 60;
  659. _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
  660. _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds));
  661. }
  662. else
  663. {
  664. _timerProvider.Delete(timer);
  665. _recordingProvider.Delete(recording);
  666. }
  667. }
  668. private async void OnSuccessfulRecording(RecordingInfo recording)
  669. {
  670. if (GetConfiguration().EnableAutoOrganize)
  671. {
  672. if (recording.IsSeries)
  673. {
  674. try
  675. {
  676. var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
  677. var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false);
  678. if (result.Status == FileSortingStatus.Success)
  679. {
  680. _recordingProvider.Delete(recording);
  681. }
  682. }
  683. catch (Exception ex)
  684. {
  685. _logger.ErrorException("Error processing new recording", ex);
  686. }
  687. }
  688. }
  689. }
  690. private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
  691. {
  692. var epgData = GetEpgDataForChannel(channelId);
  693. return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
  694. }
  695. private string RecordingPath
  696. {
  697. get
  698. {
  699. var path = GetConfiguration().RecordingPath;
  700. return string.IsNullOrWhiteSpace(path)
  701. ? Path.Combine(DataPath, "recordings")
  702. : path;
  703. }
  704. }
  705. private LiveTvOptions GetConfiguration()
  706. {
  707. return _config.GetConfiguration<LiveTvOptions>("livetv");
  708. }
  709. private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer)
  710. {
  711. var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
  712. var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
  713. if (registration.IsValid)
  714. {
  715. foreach (var timer in newTimers)
  716. {
  717. _timerProvider.AddOrUpdate(timer);
  718. }
  719. }
  720. }
  721. private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings)
  722. {
  723. // Exclude programs that have already ended
  724. allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow);
  725. allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
  726. var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
  727. allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase));
  728. return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
  729. }
  730. private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
  731. {
  732. if (!seriesTimer.RecordAnyTime)
  733. {
  734. allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks);
  735. }
  736. if (seriesTimer.RecordNewOnly)
  737. {
  738. allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
  739. }
  740. if (!seriesTimer.RecordAnyChannel)
  741. {
  742. allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
  743. }
  744. allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
  745. if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
  746. {
  747. _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
  748. return new List<ProgramInfo>();
  749. }
  750. return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
  751. }
  752. private string GetChannelEpgCachePath(string channelId)
  753. {
  754. return Path.Combine(_config.CommonApplicationPaths.CachePath, "embytvepg", channelId + ".json");
  755. }
  756. private readonly object _epgLock = new object();
  757. private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
  758. {
  759. var path = GetChannelEpgCachePath(channelId);
  760. _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
  761. lock (_epgLock)
  762. {
  763. _jsonSerializer.SerializeToFile(epgData, path);
  764. }
  765. }
  766. private List<ProgramInfo> GetEpgDataForChannel(string channelId)
  767. {
  768. try
  769. {
  770. lock (_epgLock)
  771. {
  772. return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
  773. }
  774. }
  775. catch
  776. {
  777. return new List<ProgramInfo>();
  778. }
  779. }
  780. private List<ProgramInfo> GetEpgDataForChannels(List<string> channelIds)
  781. {
  782. return channelIds.SelectMany(GetEpgDataForChannel).ToList();
  783. }
  784. public void Dispose()
  785. {
  786. foreach (var pair in _activeRecordings.ToList())
  787. {
  788. pair.Value.Cancel();
  789. }
  790. }
  791. public Task<MBRegistrationRecord> GetRegistrationInfo(string feature)
  792. {
  793. if (string.Equals(feature, "seriesrecordings", StringComparison.OrdinalIgnoreCase))
  794. {
  795. return _security.GetRegistrationStatus("embytvseriesrecordings");
  796. }
  797. return Task.FromResult(new MBRegistrationRecord
  798. {
  799. IsValid = true,
  800. IsRegistered = true
  801. });
  802. }
  803. }
  804. }