EmbyTV.cs 30 KB

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