EmbyTV.cs 102 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730
  1. using MediaBrowser.Common;
  2. using MediaBrowser.Common.Configuration;
  3. using MediaBrowser.Common.Net;
  4. using MediaBrowser.Common.Security;
  5. using MediaBrowser.Controller.Configuration;
  6. using MediaBrowser.Controller.Drawing;
  7. using MediaBrowser.Controller.FileOrganization;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Controller.LiveTv;
  10. using MediaBrowser.Controller.MediaEncoding;
  11. using MediaBrowser.Controller.Providers;
  12. using MediaBrowser.Model.Dto;
  13. using MediaBrowser.Model.Entities;
  14. using MediaBrowser.Model.Events;
  15. using MediaBrowser.Model.LiveTv;
  16. using MediaBrowser.Model.Logging;
  17. using MediaBrowser.Model.Serialization;
  18. using System;
  19. using System.Collections.Concurrent;
  20. using System.Collections.Generic;
  21. using System.Globalization;
  22. using System.IO;
  23. using System.Linq;
  24. using System.Text;
  25. using System.Threading;
  26. using System.Threading.Tasks;
  27. using System.Xml;
  28. using MediaBrowser.Model.IO;
  29. using MediaBrowser.Common.Events;
  30. using MediaBrowser.Common.Extensions;
  31. using MediaBrowser.Common.IO;
  32. using MediaBrowser.Controller;
  33. using MediaBrowser.Controller.Dto;
  34. using MediaBrowser.Controller.Entities;
  35. using MediaBrowser.Controller.Entities.TV;
  36. using MediaBrowser.Controller.IO;
  37. using MediaBrowser.Model.Configuration;
  38. using MediaBrowser.Model.Diagnostics;
  39. using MediaBrowser.Model.FileOrganization;
  40. using MediaBrowser.Model.System;
  41. using MediaBrowser.Model.Threading;
  42. using MediaBrowser.Model.Extensions;
  43. using MediaBrowser.Model.Querying;
  44. namespace Emby.Server.Implementations.LiveTv.EmbyTV
  45. {
  46. public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
  47. {
  48. private readonly IServerApplicationHost _appHost;
  49. private readonly ILogger _logger;
  50. private readonly IHttpClient _httpClient;
  51. private readonly IServerConfigurationManager _config;
  52. private readonly IJsonSerializer _jsonSerializer;
  53. private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
  54. private readonly TimerManager _timerProvider;
  55. private readonly LiveTvManager _liveTvManager;
  56. private readonly IFileSystem _fileSystem;
  57. private readonly ILibraryMonitor _libraryMonitor;
  58. private readonly ILibraryManager _libraryManager;
  59. private readonly IProviderManager _providerManager;
  60. private readonly IFileOrganizationService _organizationService;
  61. private readonly IMediaEncoder _mediaEncoder;
  62. private readonly IProcessFactory _processFactory;
  63. private readonly ISystemEvents _systemEvents;
  64. public static EmbyTV Current;
  65. public event EventHandler DataSourceChanged;
  66. public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
  67. private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
  68. new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
  69. public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents)
  70. {
  71. Current = this;
  72. _appHost = appHost;
  73. _logger = logger;
  74. _httpClient = httpClient;
  75. _config = config;
  76. _fileSystem = fileSystem;
  77. _libraryManager = libraryManager;
  78. _libraryMonitor = libraryMonitor;
  79. _providerManager = providerManager;
  80. _organizationService = organizationService;
  81. _mediaEncoder = mediaEncoder;
  82. _processFactory = processFactory;
  83. _systemEvents = systemEvents;
  84. _liveTvManager = (LiveTvManager)liveTvManager;
  85. _jsonSerializer = jsonSerializer;
  86. _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
  87. _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger, timerFactory);
  88. _timerProvider.TimerFired += _timerProvider_TimerFired;
  89. _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
  90. }
  91. private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
  92. {
  93. if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
  94. {
  95. OnRecordingFoldersChanged();
  96. }
  97. }
  98. public void Start()
  99. {
  100. _timerProvider.RestartTimers();
  101. _systemEvents.Resume += _systemEvents_Resume;
  102. CreateRecordingFolders();
  103. }
  104. private void _systemEvents_Resume(object sender, EventArgs e)
  105. {
  106. _timerProvider.RestartTimers();
  107. }
  108. private void OnRecordingFoldersChanged()
  109. {
  110. CreateRecordingFolders();
  111. }
  112. internal void CreateRecordingFolders()
  113. {
  114. try
  115. {
  116. CreateRecordingFoldersInternal();
  117. }
  118. catch (Exception ex)
  119. {
  120. _logger.ErrorException("Error creating recording folders", ex);
  121. }
  122. }
  123. internal void CreateRecordingFoldersInternal()
  124. {
  125. var recordingFolders = GetRecordingFolders();
  126. var virtualFolders = _libraryManager.GetVirtualFolders()
  127. .ToList();
  128. var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
  129. var pathsAdded = new List<string>();
  130. foreach (var recordingFolder in recordingFolders)
  131. {
  132. var pathsToCreate = recordingFolder.Locations
  133. .Where(i => !allExistingPaths.Any(p => _fileSystem.AreEqual(p, i)))
  134. .ToList();
  135. if (pathsToCreate.Count == 0)
  136. {
  137. continue;
  138. }
  139. var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
  140. var libraryOptions = new LibraryOptions
  141. {
  142. PathInfos = mediaPathInfos
  143. };
  144. try
  145. {
  146. _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true);
  147. }
  148. catch (Exception ex)
  149. {
  150. _logger.ErrorException("Error creating virtual folder", ex);
  151. }
  152. pathsAdded.AddRange(pathsToCreate);
  153. }
  154. var config = GetConfiguration();
  155. var pathsToRemove = config.MediaLocationsCreated
  156. .Except(recordingFolders.SelectMany(i => i.Locations))
  157. .ToList();
  158. if (pathsAdded.Count > 0 || pathsToRemove.Count > 0)
  159. {
  160. pathsAdded.InsertRange(0, config.MediaLocationsCreated);
  161. config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  162. _config.SaveConfiguration("livetv", config);
  163. }
  164. foreach (var path in pathsToRemove)
  165. {
  166. RemovePathFromLibrary(path);
  167. }
  168. }
  169. private void RemovePathFromLibrary(string path)
  170. {
  171. _logger.Debug("Removing path from library: {0}", path);
  172. var requiresRefresh = false;
  173. var virtualFolders = _libraryManager.GetVirtualFolders()
  174. .ToList();
  175. foreach (var virtualFolder in virtualFolders)
  176. {
  177. if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
  178. {
  179. continue;
  180. }
  181. if (virtualFolder.Locations.Count == 1)
  182. {
  183. // remove entire virtual folder
  184. try
  185. {
  186. _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true);
  187. }
  188. catch (Exception ex)
  189. {
  190. _logger.ErrorException("Error removing virtual folder", ex);
  191. }
  192. }
  193. else
  194. {
  195. try
  196. {
  197. _libraryManager.RemoveMediaPath(virtualFolder.Name, path);
  198. requiresRefresh = true;
  199. }
  200. catch (Exception ex)
  201. {
  202. _logger.ErrorException("Error removing media path", ex);
  203. }
  204. }
  205. }
  206. if (requiresRefresh)
  207. {
  208. _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None);
  209. }
  210. }
  211. public string Name
  212. {
  213. get { return "Emby"; }
  214. }
  215. public string DataPath
  216. {
  217. get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
  218. }
  219. private string DefaultRecordingPath
  220. {
  221. get
  222. {
  223. return Path.Combine(DataPath, "recordings");
  224. }
  225. }
  226. private string RecordingPath
  227. {
  228. get
  229. {
  230. var path = GetConfiguration().RecordingPath;
  231. return string.IsNullOrWhiteSpace(path)
  232. ? DefaultRecordingPath
  233. : path;
  234. }
  235. }
  236. public string HomePageUrl
  237. {
  238. get { return "http://emby.media"; }
  239. }
  240. public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
  241. {
  242. var status = new LiveTvServiceStatusInfo();
  243. var list = new List<LiveTvTunerInfo>();
  244. foreach (var hostInstance in _liveTvManager.TunerHosts)
  245. {
  246. try
  247. {
  248. var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
  249. list.AddRange(tuners);
  250. }
  251. catch (Exception ex)
  252. {
  253. _logger.ErrorException("Error getting tuners", ex);
  254. }
  255. }
  256. status.Tuners = list;
  257. status.Status = LiveTvServiceStatus.Ok;
  258. status.Version = _appHost.ApplicationVersion.ToString();
  259. status.IsVisible = false;
  260. return status;
  261. }
  262. public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
  263. {
  264. var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
  265. List<ChannelInfo> channels = null;
  266. foreach (var timer in seriesTimers)
  267. {
  268. List<ProgramInfo> epgData;
  269. if (timer.RecordAnyChannel)
  270. {
  271. if (channels == null)
  272. {
  273. channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList();
  274. }
  275. var channelIds = channels.Select(i => i.Id).ToList();
  276. epgData = GetEpgDataForChannels(channelIds);
  277. }
  278. else
  279. {
  280. epgData = GetEpgDataForChannel(timer.ChannelId);
  281. }
  282. await UpdateTimersForSeriesTimer(epgData, timer, false, true).ConfigureAwait(false);
  283. }
  284. }
  285. public async Task RefreshTimers(CancellationToken cancellationToken, IProgress<double> progress)
  286. {
  287. var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
  288. foreach (var timer in timers)
  289. {
  290. if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
  291. {
  292. OnTimerOutOfDate(timer);
  293. continue;
  294. }
  295. if (string.IsNullOrWhiteSpace(timer.ProgramId) || string.IsNullOrWhiteSpace(timer.ChannelId))
  296. {
  297. continue;
  298. }
  299. var epg = GetEpgDataForChannel(timer.ChannelId);
  300. var program = epg.FirstOrDefault(i => string.Equals(i.Id, timer.ProgramId, StringComparison.OrdinalIgnoreCase));
  301. if (program == null)
  302. {
  303. OnTimerOutOfDate(timer);
  304. continue;
  305. }
  306. RecordingHelper.CopyProgramInfoToTimerInfo(program, timer);
  307. _timerProvider.Update(timer);
  308. }
  309. }
  310. private void OnTimerOutOfDate(TimerInfo timer)
  311. {
  312. _timerProvider.Delete(timer);
  313. }
  314. private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
  315. {
  316. var list = new List<ChannelInfo>();
  317. foreach (var hostInstance in _liveTvManager.TunerHosts)
  318. {
  319. try
  320. {
  321. var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
  322. list.AddRange(channels);
  323. }
  324. catch (Exception ex)
  325. {
  326. _logger.ErrorException("Error getting channels", ex);
  327. }
  328. }
  329. foreach (var provider in GetListingProviders())
  330. {
  331. var enabledChannels = list
  332. .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
  333. .ToList();
  334. if (enabledChannels.Count > 0)
  335. {
  336. try
  337. {
  338. await AddMetadata(provider.Item1, provider.Item2, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false);
  339. }
  340. catch (NotSupportedException)
  341. {
  342. }
  343. catch (Exception ex)
  344. {
  345. _logger.ErrorException("Error adding metadata", ex);
  346. }
  347. }
  348. }
  349. return list;
  350. }
  351. private async Task AddMetadata(IListingsProvider provider, ListingsProviderInfo info, List<ChannelInfo> tunerChannels, bool enableCache, CancellationToken cancellationToken)
  352. {
  353. var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
  354. foreach (var tunerChannel in tunerChannels)
  355. {
  356. var epgChannel = GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
  357. if (epgChannel != null)
  358. {
  359. if (!string.IsNullOrWhiteSpace(epgChannel.Name))
  360. {
  361. //tunerChannel.Name = epgChannel.Name;
  362. }
  363. if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
  364. {
  365. tunerChannel.ImageUrl = epgChannel.ImageUrl;
  366. tunerChannel.HasImage = true;
  367. }
  368. }
  369. }
  370. }
  371. private readonly ConcurrentDictionary<string, List<ChannelInfo>> _epgChannels =
  372. new ConcurrentDictionary<string, List<ChannelInfo>>(StringComparer.OrdinalIgnoreCase);
  373. private async Task<List<ChannelInfo>> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken)
  374. {
  375. List<ChannelInfo> result;
  376. if (!enableCache || !_epgChannels.TryGetValue(info.Id, out result))
  377. {
  378. result = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
  379. foreach (var channel in result)
  380. {
  381. _logger.Info("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
  382. }
  383. _epgChannels.AddOrUpdate(info.Id, result, (k, v) => result);
  384. }
  385. return result;
  386. }
  387. private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken)
  388. {
  389. var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false);
  390. return GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
  391. }
  392. private string GetMappedChannel(string channelId, List<NameValuePair> mappings)
  393. {
  394. foreach (NameValuePair mapping in mappings)
  395. {
  396. if (StringHelper.EqualsIgnoreCase(mapping.Name, channelId))
  397. {
  398. return mapping.Value;
  399. }
  400. }
  401. return channelId;
  402. }
  403. private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
  404. {
  405. return GetEpgChannelFromTunerChannel(info.ChannelMappings.ToList(), tunerChannel, epgChannels);
  406. }
  407. public ChannelInfo GetEpgChannelFromTunerChannel(List<NameValuePair> mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
  408. {
  409. if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
  410. {
  411. var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
  412. if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
  413. {
  414. mappedTunerChannelId = tunerChannel.Id;
  415. }
  416. var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
  417. if (channel != null)
  418. {
  419. return channel;
  420. }
  421. }
  422. if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
  423. {
  424. var tunerChannelId = tunerChannel.TunerChannelId;
  425. if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
  426. {
  427. tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
  428. }
  429. var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
  430. if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
  431. {
  432. mappedTunerChannelId = tunerChannelId;
  433. }
  434. var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
  435. if (channel != null)
  436. {
  437. return channel;
  438. }
  439. }
  440. if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
  441. {
  442. var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
  443. if (string.IsNullOrWhiteSpace(tunerChannelNumber))
  444. {
  445. tunerChannelNumber = tunerChannel.Number;
  446. }
  447. var channel = epgChannels.FirstOrDefault(i => string.Equals(tunerChannelNumber, i.Number, StringComparison.OrdinalIgnoreCase));
  448. if (channel != null)
  449. {
  450. return channel;
  451. }
  452. }
  453. if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
  454. {
  455. var normalizedName = NormalizeName(tunerChannel.Name);
  456. var channel = epgChannels.FirstOrDefault(i => string.Equals(normalizedName, NormalizeName(i.Name ?? string.Empty), StringComparison.OrdinalIgnoreCase));
  457. if (channel != null)
  458. {
  459. return channel;
  460. }
  461. }
  462. return null;
  463. }
  464. private string NormalizeName(string value)
  465. {
  466. return value.Replace(" ", string.Empty).Replace("-", string.Empty);
  467. }
  468. public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
  469. {
  470. var list = new List<ChannelInfo>();
  471. foreach (var hostInstance in _liveTvManager.TunerHosts)
  472. {
  473. try
  474. {
  475. var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
  476. list.AddRange(channels);
  477. }
  478. catch (Exception ex)
  479. {
  480. _logger.ErrorException("Error getting channels", ex);
  481. }
  482. }
  483. return list
  484. .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId))
  485. .ToList();
  486. }
  487. public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
  488. {
  489. return GetChannelsAsync(false, cancellationToken);
  490. }
  491. public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
  492. {
  493. var timers = _timerProvider
  494. .GetAll()
  495. .Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
  496. .ToList();
  497. foreach (var timer in timers)
  498. {
  499. CancelTimerInternal(timer.Id, true);
  500. }
  501. var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
  502. if (remove != null)
  503. {
  504. _seriesTimerProvider.Delete(remove);
  505. }
  506. return Task.FromResult(true);
  507. }
  508. private void CancelTimerInternal(string timerId, bool isSeriesCancelled)
  509. {
  510. var timer = _timerProvider.GetTimer(timerId);
  511. if (timer != null)
  512. {
  513. if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
  514. {
  515. _timerProvider.Delete(timer);
  516. }
  517. else
  518. {
  519. timer.Status = RecordingStatus.Cancelled;
  520. _timerProvider.AddOrUpdate(timer, false);
  521. }
  522. }
  523. ActiveRecordingInfo activeRecordingInfo;
  524. if (_activeRecordings.TryGetValue(timerId, out activeRecordingInfo))
  525. {
  526. activeRecordingInfo.CancellationTokenSource.Cancel();
  527. }
  528. }
  529. public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
  530. {
  531. CancelTimerInternal(timerId, false);
  532. return Task.FromResult(true);
  533. }
  534. public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
  535. {
  536. return Task.FromResult(true);
  537. }
  538. public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  539. {
  540. throw new NotImplementedException();
  541. }
  542. public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
  543. {
  544. throw new NotImplementedException();
  545. }
  546. public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
  547. {
  548. var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
  549. null :
  550. _timerProvider.GetTimerByProgramId(timer.ProgramId);
  551. if (existingTimer != null)
  552. {
  553. if (existingTimer.Status == RecordingStatus.Cancelled ||
  554. existingTimer.Status == RecordingStatus.Completed)
  555. {
  556. existingTimer.Status = RecordingStatus.New;
  557. existingTimer.IsManual = true;
  558. _timerProvider.Update(existingTimer);
  559. return Task.FromResult(existingTimer.Id);
  560. }
  561. else
  562. {
  563. throw new ArgumentException("A scheduled recording already exists for this program.");
  564. }
  565. }
  566. timer.Id = Guid.NewGuid().ToString("N");
  567. ProgramInfo programInfo = null;
  568. if (!string.IsNullOrWhiteSpace(timer.ProgramId))
  569. {
  570. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  571. }
  572. if (programInfo == null)
  573. {
  574. _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
  575. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
  576. }
  577. if (programInfo != null)
  578. {
  579. RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
  580. }
  581. timer.IsManual = true;
  582. _timerProvider.Add(timer);
  583. return Task.FromResult(timer.Id);
  584. }
  585. public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
  586. {
  587. info.Id = Guid.NewGuid().ToString("N");
  588. List<ProgramInfo> epgData;
  589. if (info.RecordAnyChannel)
  590. {
  591. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  592. var channelIds = channels.Select(i => i.Id).ToList();
  593. epgData = GetEpgDataForChannels(channelIds);
  594. }
  595. else
  596. {
  597. epgData = GetEpgDataForChannel(info.ChannelId);
  598. }
  599. // populate info.seriesID
  600. var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
  601. if (program != null)
  602. {
  603. info.SeriesId = program.SeriesId;
  604. }
  605. else
  606. {
  607. throw new InvalidOperationException("SeriesId for program not found");
  608. }
  609. // If any timers have already been manually created, make sure they don't get cancelled
  610. var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false))
  611. .Where(i =>
  612. {
  613. if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.ProgramId))
  614. {
  615. return true;
  616. }
  617. if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.SeriesId))
  618. {
  619. return true;
  620. }
  621. return false;
  622. })
  623. .ToList();
  624. _seriesTimerProvider.Add(info);
  625. foreach (var timer in existingTimers)
  626. {
  627. timer.SeriesTimerId = info.Id;
  628. timer.IsManual = true;
  629. _timerProvider.AddOrUpdate(timer, false);
  630. }
  631. await UpdateTimersForSeriesTimer(epgData, info, true, false).ConfigureAwait(false);
  632. return info.Id;
  633. }
  634. public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
  635. {
  636. var instance = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
  637. if (instance != null)
  638. {
  639. instance.ChannelId = info.ChannelId;
  640. instance.Days = info.Days;
  641. instance.EndDate = info.EndDate;
  642. instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
  643. instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
  644. instance.PostPaddingSeconds = info.PostPaddingSeconds;
  645. instance.PrePaddingSeconds = info.PrePaddingSeconds;
  646. instance.Priority = info.Priority;
  647. instance.RecordAnyChannel = info.RecordAnyChannel;
  648. instance.RecordAnyTime = info.RecordAnyTime;
  649. instance.RecordNewOnly = info.RecordNewOnly;
  650. instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
  651. instance.KeepUpTo = info.KeepUpTo;
  652. instance.KeepUntil = info.KeepUntil;
  653. instance.StartDate = info.StartDate;
  654. _seriesTimerProvider.Update(instance);
  655. List<ProgramInfo> epgData;
  656. if (instance.RecordAnyChannel)
  657. {
  658. var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
  659. var channelIds = channels.Select(i => i.Id).ToList();
  660. epgData = GetEpgDataForChannels(channelIds);
  661. }
  662. else
  663. {
  664. epgData = GetEpgDataForChannel(instance.ChannelId);
  665. }
  666. await UpdateTimersForSeriesTimer(epgData, instance, true, true).ConfigureAwait(false);
  667. }
  668. }
  669. public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
  670. {
  671. var existingTimer = _timerProvider.GetTimer(updatedTimer.Id);
  672. if (existingTimer == null)
  673. {
  674. throw new ResourceNotFoundException();
  675. }
  676. // Only update if not currently active
  677. ActiveRecordingInfo activeRecordingInfo;
  678. if (!_activeRecordings.TryGetValue(updatedTimer.Id, out activeRecordingInfo))
  679. {
  680. existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
  681. existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
  682. existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
  683. existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
  684. _timerProvider.Update(existingTimer);
  685. }
  686. return Task.FromResult(true);
  687. }
  688. private void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
  689. {
  690. // Update the program info but retain the status
  691. existingTimer.ChannelId = updatedTimer.ChannelId;
  692. existingTimer.CommunityRating = updatedTimer.CommunityRating;
  693. existingTimer.EndDate = updatedTimer.EndDate;
  694. existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
  695. existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
  696. existingTimer.Genres = updatedTimer.Genres;
  697. existingTimer.HomePageUrl = updatedTimer.HomePageUrl;
  698. existingTimer.IsKids = updatedTimer.IsKids;
  699. existingTimer.IsNews = updatedTimer.IsNews;
  700. existingTimer.IsMovie = updatedTimer.IsMovie;
  701. existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
  702. existingTimer.IsRepeat = updatedTimer.IsRepeat;
  703. existingTimer.IsSports = updatedTimer.IsSports;
  704. existingTimer.Name = updatedTimer.Name;
  705. existingTimer.OfficialRating = updatedTimer.OfficialRating;
  706. existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
  707. existingTimer.Overview = updatedTimer.Overview;
  708. existingTimer.ProductionYear = updatedTimer.ProductionYear;
  709. existingTimer.ProgramId = updatedTimer.ProgramId;
  710. existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
  711. existingTimer.StartDate = updatedTimer.StartDate;
  712. existingTimer.ShowId = updatedTimer.ShowId;
  713. }
  714. public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
  715. {
  716. throw new NotImplementedException();
  717. }
  718. public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
  719. {
  720. throw new NotImplementedException();
  721. }
  722. public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
  723. {
  724. throw new NotImplementedException();
  725. }
  726. public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
  727. {
  728. return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList();
  729. }
  730. public string GetActiveRecordingPath(string id)
  731. {
  732. ActiveRecordingInfo info;
  733. if (_activeRecordings.TryGetValue(id, out info))
  734. {
  735. return info.Path;
  736. }
  737. return null;
  738. }
  739. private RecordingInfo GetRecordingInfo(ActiveRecordingInfo info)
  740. {
  741. var timer = info.Timer;
  742. var program = info.Program;
  743. var result = new RecordingInfo
  744. {
  745. ChannelId = timer.ChannelId,
  746. CommunityRating = timer.CommunityRating,
  747. DateLastUpdated = DateTime.UtcNow,
  748. EndDate = timer.EndDate,
  749. EpisodeTitle = timer.EpisodeTitle,
  750. Genres = timer.Genres,
  751. Id = "recording" + timer.Id,
  752. IsKids = timer.IsKids,
  753. IsMovie = timer.IsMovie,
  754. IsNews = timer.IsNews,
  755. IsRepeat = timer.IsRepeat,
  756. IsSeries = timer.IsProgramSeries,
  757. IsSports = timer.IsSports,
  758. Name = timer.Name,
  759. OfficialRating = timer.OfficialRating,
  760. OriginalAirDate = timer.OriginalAirDate,
  761. Overview = timer.Overview,
  762. ProgramId = timer.ProgramId,
  763. SeriesTimerId = timer.SeriesTimerId,
  764. StartDate = timer.StartDate,
  765. Status = RecordingStatus.InProgress,
  766. TimerId = timer.Id
  767. };
  768. if (program != null)
  769. {
  770. result.Audio = program.Audio;
  771. result.ImagePath = program.ImagePath;
  772. result.ImageUrl = program.ImageUrl;
  773. result.IsHD = program.IsHD;
  774. result.IsLive = program.IsLive;
  775. result.IsPremiere = program.IsPremiere;
  776. result.ShowId = program.ShowId;
  777. }
  778. return result;
  779. }
  780. public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
  781. {
  782. var excludeStatues = new List<RecordingStatus>
  783. {
  784. RecordingStatus.Completed
  785. };
  786. var timers = _timerProvider.GetAll()
  787. .Where(i => !excludeStatues.Contains(i.Status));
  788. return Task.FromResult(timers);
  789. }
  790. public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
  791. {
  792. var config = GetConfiguration();
  793. var defaults = new SeriesTimerInfo()
  794. {
  795. PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
  796. PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
  797. RecordAnyChannel = false,
  798. RecordAnyTime = true,
  799. RecordNewOnly = true,
  800. Days = new List<DayOfWeek>
  801. {
  802. DayOfWeek.Sunday,
  803. DayOfWeek.Monday,
  804. DayOfWeek.Tuesday,
  805. DayOfWeek.Wednesday,
  806. DayOfWeek.Thursday,
  807. DayOfWeek.Friday,
  808. DayOfWeek.Saturday
  809. }
  810. };
  811. if (program != null)
  812. {
  813. defaults.SeriesId = program.SeriesId;
  814. defaults.ProgramId = program.Id;
  815. defaults.RecordNewOnly = !program.IsRepeat;
  816. }
  817. defaults.SkipEpisodesInLibrary = defaults.RecordNewOnly;
  818. defaults.KeepUntil = KeepUntil.UntilDeleted;
  819. return Task.FromResult(defaults);
  820. }
  821. public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
  822. {
  823. return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
  824. }
  825. public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  826. {
  827. try
  828. {
  829. return await GetProgramsAsyncInternal(channelId, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false);
  830. }
  831. catch (OperationCanceledException)
  832. {
  833. throw;
  834. }
  835. catch (Exception ex)
  836. {
  837. _logger.ErrorException("Error getting programs", ex);
  838. return GetEpgDataForChannel(channelId).Where(i => i.StartDate <= endDateUtc && i.EndDate >= startDateUtc);
  839. }
  840. }
  841. private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
  842. {
  843. if (info.EnableAllTuners)
  844. {
  845. return true;
  846. }
  847. if (string.IsNullOrWhiteSpace(tunerHostId))
  848. {
  849. throw new ArgumentNullException("tunerHostId");
  850. }
  851. return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
  852. }
  853. private async Task<IEnumerable<ProgramInfo>> GetProgramsAsyncInternal(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
  854. {
  855. var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
  856. var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  857. foreach (var provider in GetListingProviders())
  858. {
  859. if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
  860. {
  861. _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);
  862. continue;
  863. }
  864. _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
  865. var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
  866. List<ProgramInfo> programs;
  867. if (epgChannel == null)
  868. {
  869. _logger.Debug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
  870. programs = new List<ProgramInfo>();
  871. }
  872. else
  873. {
  874. programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
  875. .ConfigureAwait(false)).ToList();
  876. }
  877. // Replace the value that came from the provider with a normalized value
  878. foreach (var program in programs)
  879. {
  880. program.ChannelId = channelId;
  881. if (provider.Item2.EnableNewProgramIds)
  882. {
  883. program.Id += "_" + channelId;
  884. }
  885. }
  886. if (programs.Count > 0)
  887. {
  888. SaveEpgDataForChannel(channelId, programs);
  889. return programs;
  890. }
  891. }
  892. return new List<ProgramInfo>();
  893. }
  894. private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
  895. {
  896. return GetConfiguration().ListingProviders
  897. .Select(i =>
  898. {
  899. var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
  900. return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
  901. })
  902. .Where(i => i != null)
  903. .ToList();
  904. }
  905. public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
  906. {
  907. throw new NotImplementedException();
  908. }
  909. private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
  910. private readonly List<LiveStream> _liveStreams = new List<LiveStream>();
  911. public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
  912. {
  913. var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false);
  914. return result.Item1;
  915. }
  916. public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken)
  917. {
  918. var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
  919. return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider);
  920. }
  921. private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
  922. {
  923. var json = _jsonSerializer.SerializeToString(mediaSource);
  924. mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
  925. mediaSource.Id = Guid.NewGuid().ToString("N") + "_" + mediaSource.Id;
  926. //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
  927. //{
  928. // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
  929. // ticks = Math.Max(0, ticks);
  930. // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
  931. //}
  932. return mediaSource;
  933. }
  934. public async Task<LiveStream> GetLiveStream(string uniqueId)
  935. {
  936. await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
  937. try
  938. {
  939. return _liveStreams
  940. .FirstOrDefault(i => string.Equals(i.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
  941. }
  942. finally
  943. {
  944. _liveStreamsSemaphore.Release();
  945. }
  946. }
  947. private async Task<Tuple<LiveStream, MediaSourceInfo, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
  948. {
  949. _logger.Info("Streaming Channel " + channelId);
  950. await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  951. try
  952. {
  953. var result = _liveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase));
  954. if (result != null && result.EnableStreamSharing)
  955. {
  956. var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
  957. result.SharedStreamIds.Add(openedMediaSource.Id);
  958. _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
  959. return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, result.TunerHost);
  960. }
  961. foreach (var hostInstance in _liveTvManager.TunerHosts)
  962. {
  963. try
  964. {
  965. result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
  966. var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
  967. result.SharedStreamIds.Add(openedMediaSource.Id);
  968. _liveStreams.Add(result);
  969. result.TunerHost = hostInstance;
  970. result.OriginalStreamId = streamId;
  971. _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}",
  972. streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId);
  973. return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, hostInstance);
  974. }
  975. catch (FileNotFoundException)
  976. {
  977. }
  978. catch (OperationCanceledException)
  979. {
  980. }
  981. }
  982. }
  983. finally
  984. {
  985. _liveStreamsSemaphore.Release();
  986. }
  987. throw new Exception("Tuner not found.");
  988. }
  989. public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
  990. {
  991. if (string.IsNullOrWhiteSpace(channelId))
  992. {
  993. throw new ArgumentNullException("channelId");
  994. }
  995. foreach (var hostInstance in _liveTvManager.TunerHosts)
  996. {
  997. try
  998. {
  999. var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
  1000. if (sources.Count > 0)
  1001. {
  1002. return sources;
  1003. }
  1004. }
  1005. catch (NotImplementedException)
  1006. {
  1007. }
  1008. }
  1009. throw new NotImplementedException();
  1010. }
  1011. public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
  1012. {
  1013. ActiveRecordingInfo info;
  1014. recordingId = recordingId.Replace("recording", string.Empty);
  1015. if (_activeRecordings.TryGetValue(recordingId, out info))
  1016. {
  1017. var stream = new MediaSourceInfo
  1018. {
  1019. Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + recordingId + "/stream",
  1020. Id = recordingId,
  1021. SupportsDirectPlay = false,
  1022. SupportsDirectStream = true,
  1023. SupportsTranscoding = true,
  1024. IsInfiniteStream = true,
  1025. RequiresOpening = false,
  1026. RequiresClosing = false,
  1027. Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http,
  1028. BufferMs = 0,
  1029. IgnoreDts = true,
  1030. IgnoreIndex = true,
  1031. GenPtsInput = true
  1032. };
  1033. var isAudio = false;
  1034. await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
  1035. return new List<MediaSourceInfo>
  1036. {
  1037. stream
  1038. };
  1039. }
  1040. throw new FileNotFoundException();
  1041. }
  1042. public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
  1043. {
  1044. // Ignore the consumer id
  1045. //id = id.Substring(id.IndexOf('_') + 1);
  1046. await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  1047. try
  1048. {
  1049. var stream = _liveStreams.FirstOrDefault(i => i.SharedStreamIds.Contains(id));
  1050. if (stream != null)
  1051. {
  1052. stream.SharedStreamIds.Remove(id);
  1053. _logger.Info("Live stream {0} consumer count is now {1}", id, stream.ConsumerCount);
  1054. if (stream.ConsumerCount <= 0)
  1055. {
  1056. _liveStreams.Remove(stream);
  1057. _logger.Info("Closing live stream {0}", id);
  1058. await stream.Close().ConfigureAwait(false);
  1059. _logger.Info("Live stream {0} closed successfully", id);
  1060. }
  1061. }
  1062. else
  1063. {
  1064. _logger.Warn("Live stream not found: {0}, unable to close", id);
  1065. }
  1066. }
  1067. catch (OperationCanceledException)
  1068. {
  1069. }
  1070. catch (Exception ex)
  1071. {
  1072. _logger.ErrorException("Error closing live stream", ex);
  1073. }
  1074. finally
  1075. {
  1076. _liveStreamsSemaphore.Release();
  1077. }
  1078. }
  1079. public Task RecordLiveStream(string id, CancellationToken cancellationToken)
  1080. {
  1081. return Task.FromResult(0);
  1082. }
  1083. public Task ResetTuner(string id, CancellationToken cancellationToken)
  1084. {
  1085. return Task.FromResult(0);
  1086. }
  1087. async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
  1088. {
  1089. var timer = e.Argument;
  1090. _logger.Info("Recording timer fired.");
  1091. try
  1092. {
  1093. var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
  1094. if (recordingEndDate <= DateTime.UtcNow)
  1095. {
  1096. _logger.Warn("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
  1097. OnTimerOutOfDate(timer);
  1098. return;
  1099. }
  1100. var registration = await _liveTvManager.GetRegistrationInfo("dvr").ConfigureAwait(false);
  1101. if (!registration.IsValid)
  1102. {
  1103. _logger.Warn("Emby Premiere required to use Emby DVR.");
  1104. OnTimerOutOfDate(timer);
  1105. return;
  1106. }
  1107. var activeRecordingInfo = new ActiveRecordingInfo
  1108. {
  1109. CancellationTokenSource = new CancellationTokenSource(),
  1110. Timer = timer
  1111. };
  1112. if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
  1113. {
  1114. await RecordStream(timer, recordingEndDate, activeRecordingInfo, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
  1115. }
  1116. else
  1117. {
  1118. _logger.Info("Skipping RecordStream because it's already in progress.");
  1119. }
  1120. }
  1121. catch (OperationCanceledException)
  1122. {
  1123. }
  1124. catch (Exception ex)
  1125. {
  1126. _logger.ErrorException("Error recording stream", ex);
  1127. }
  1128. }
  1129. private string GetRecordingPath(TimerInfo timer, out string seriesPath)
  1130. {
  1131. var recordPath = RecordingPath;
  1132. var config = GetConfiguration();
  1133. seriesPath = null;
  1134. if (timer.IsProgramSeries)
  1135. {
  1136. var customRecordingPath = config.SeriesRecordingPath;
  1137. var allowSubfolder = true;
  1138. if (!string.IsNullOrWhiteSpace(customRecordingPath))
  1139. {
  1140. allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
  1141. recordPath = customRecordingPath;
  1142. }
  1143. if (allowSubfolder && config.EnableRecordingSubfolders)
  1144. {
  1145. recordPath = Path.Combine(recordPath, "Series");
  1146. }
  1147. var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
  1148. // Can't use the year here in the folder name because it is the year of the episode, not the series.
  1149. recordPath = Path.Combine(recordPath, folderName);
  1150. seriesPath = recordPath;
  1151. if (timer.SeasonNumber.HasValue)
  1152. {
  1153. folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
  1154. recordPath = Path.Combine(recordPath, folderName);
  1155. }
  1156. }
  1157. else if (timer.IsMovie)
  1158. {
  1159. var customRecordingPath = config.MovieRecordingPath;
  1160. var allowSubfolder = true;
  1161. if (!string.IsNullOrWhiteSpace(customRecordingPath))
  1162. {
  1163. allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
  1164. recordPath = customRecordingPath;
  1165. }
  1166. if (allowSubfolder && config.EnableRecordingSubfolders)
  1167. {
  1168. recordPath = Path.Combine(recordPath, "Movies");
  1169. }
  1170. var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
  1171. if (timer.ProductionYear.HasValue)
  1172. {
  1173. folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  1174. }
  1175. recordPath = Path.Combine(recordPath, folderName);
  1176. }
  1177. else if (timer.IsKids)
  1178. {
  1179. if (config.EnableRecordingSubfolders)
  1180. {
  1181. recordPath = Path.Combine(recordPath, "Kids");
  1182. }
  1183. var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
  1184. if (timer.ProductionYear.HasValue)
  1185. {
  1186. folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
  1187. }
  1188. recordPath = Path.Combine(recordPath, folderName);
  1189. }
  1190. else if (timer.IsSports)
  1191. {
  1192. if (config.EnableRecordingSubfolders)
  1193. {
  1194. recordPath = Path.Combine(recordPath, "Sports");
  1195. }
  1196. recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
  1197. }
  1198. else
  1199. {
  1200. if (config.EnableRecordingSubfolders)
  1201. {
  1202. recordPath = Path.Combine(recordPath, "Other");
  1203. }
  1204. recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
  1205. }
  1206. var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts";
  1207. return Path.Combine(recordPath, recordingFileName);
  1208. }
  1209. private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate,
  1210. ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
  1211. {
  1212. if (timer == null)
  1213. {
  1214. throw new ArgumentNullException("timer");
  1215. }
  1216. ProgramInfo programInfo = null;
  1217. if (!string.IsNullOrWhiteSpace(timer.ProgramId))
  1218. {
  1219. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
  1220. }
  1221. if (programInfo == null)
  1222. {
  1223. _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
  1224. programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
  1225. }
  1226. if (programInfo != null)
  1227. {
  1228. RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
  1229. activeRecordingInfo.Program = programInfo;
  1230. }
  1231. string seriesPath = null;
  1232. var recordPath = GetRecordingPath(timer, out seriesPath);
  1233. var recordingStatus = RecordingStatus.New;
  1234. string liveStreamId = null;
  1235. OnRecordingStatusChanged();
  1236. try
  1237. {
  1238. var recorder = await GetRecorder().ConfigureAwait(false);
  1239. var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false);
  1240. var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None)
  1241. .ConfigureAwait(false);
  1242. var mediaStreamInfo = liveStreamInfo.Item2;
  1243. liveStreamId = mediaStreamInfo.Id;
  1244. // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
  1245. //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
  1246. recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath);
  1247. recordPath = EnsureFileUnique(recordPath, timer.Id);
  1248. _libraryManager.RegisterIgnoredPath(recordPath);
  1249. _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
  1250. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(recordPath));
  1251. activeRecordingInfo.Path = recordPath;
  1252. var duration = recordingEndDate - DateTime.UtcNow;
  1253. _logger.Info("Beginning recording. Will record for {0} minutes.",
  1254. duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
  1255. _logger.Info("Writing file to path: " + recordPath);
  1256. _logger.Info("Opening recording stream from tuner provider");
  1257. Action onStarted = () =>
  1258. {
  1259. timer.Status = RecordingStatus.InProgress;
  1260. _timerProvider.AddOrUpdate(timer, false);
  1261. SaveRecordingMetadata(timer, recordPath, seriesPath);
  1262. EnforceKeepUpTo(timer, seriesPath);
  1263. };
  1264. await recorder.Record(liveStreamInfo.Item1 as IDirectStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
  1265. recordingStatus = RecordingStatus.Completed;
  1266. _logger.Info("Recording completed: {0}", recordPath);
  1267. }
  1268. catch (OperationCanceledException)
  1269. {
  1270. _logger.Info("Recording stopped: {0}", recordPath);
  1271. recordingStatus = RecordingStatus.Completed;
  1272. }
  1273. catch (Exception ex)
  1274. {
  1275. _logger.ErrorException("Error recording to {0}", ex, recordPath);
  1276. recordingStatus = RecordingStatus.Error;
  1277. }
  1278. if (!string.IsNullOrWhiteSpace(liveStreamId))
  1279. {
  1280. try
  1281. {
  1282. await CloseLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
  1283. }
  1284. catch (Exception ex)
  1285. {
  1286. _logger.ErrorException("Error closing live stream", ex);
  1287. }
  1288. }
  1289. _libraryManager.UnRegisterIgnoredPath(recordPath);
  1290. _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
  1291. ActiveRecordingInfo removed;
  1292. _activeRecordings.TryRemove(timer.Id, out removed);
  1293. if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10)
  1294. {
  1295. const int retryIntervalSeconds = 60;
  1296. _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
  1297. timer.Status = RecordingStatus.New;
  1298. timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
  1299. timer.RetryCount++;
  1300. _timerProvider.AddOrUpdate(timer);
  1301. }
  1302. else if (_fileSystem.FileExists(recordPath))
  1303. {
  1304. timer.RecordingPath = recordPath;
  1305. timer.Status = RecordingStatus.Completed;
  1306. _timerProvider.AddOrUpdate(timer, false);
  1307. OnSuccessfulRecording(timer, recordPath);
  1308. }
  1309. else
  1310. {
  1311. _timerProvider.Delete(timer);
  1312. }
  1313. OnRecordingStatusChanged();
  1314. }
  1315. private void OnRecordingStatusChanged()
  1316. {
  1317. EventHelper.FireEventIfNotNull(RecordingStatusChanged, this, new RecordingStatusChangedEventArgs
  1318. {
  1319. }, _logger);
  1320. }
  1321. private async void EnforceKeepUpTo(TimerInfo timer, string seriesPath)
  1322. {
  1323. if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
  1324. {
  1325. return;
  1326. }
  1327. if (string.IsNullOrWhiteSpace(seriesPath))
  1328. {
  1329. return;
  1330. }
  1331. var seriesTimerId = timer.SeriesTimerId;
  1332. var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase));
  1333. if (seriesTimer == null || seriesTimer.KeepUpTo <= 1)
  1334. {
  1335. return;
  1336. }
  1337. if (_disposed)
  1338. {
  1339. return;
  1340. }
  1341. await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
  1342. try
  1343. {
  1344. if (_disposed)
  1345. {
  1346. return;
  1347. }
  1348. var timersToDelete = _timerProvider.GetAll()
  1349. .Where(i => i.Status == RecordingStatus.Completed && !string.IsNullOrWhiteSpace(i.RecordingPath))
  1350. .Where(i => string.Equals(i.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase))
  1351. .OrderByDescending(i => i.EndDate)
  1352. .Where(i => _fileSystem.FileExists(i.RecordingPath))
  1353. .Skip(seriesTimer.KeepUpTo - 1)
  1354. .ToList();
  1355. await DeleteLibraryItemsForTimers(timersToDelete).ConfigureAwait(false);
  1356. var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
  1357. if (librarySeries == null)
  1358. {
  1359. return;
  1360. }
  1361. var episodesToDelete = (await librarySeries.GetItems(new InternalItemsQuery
  1362. {
  1363. SortBy = new[] { ItemSortBy.DateCreated },
  1364. SortOrder = SortOrder.Descending,
  1365. IsVirtualItem = false,
  1366. IsFolder = false,
  1367. Recursive = true,
  1368. DtoOptions = new DtoOptions(true)
  1369. }).ConfigureAwait(false))
  1370. .Items
  1371. .Where(i => i.LocationType == LocationType.FileSystem && _fileSystem.FileExists(i.Path))
  1372. .Skip(seriesTimer.KeepUpTo - 1)
  1373. .ToList();
  1374. foreach (var item in episodesToDelete)
  1375. {
  1376. try
  1377. {
  1378. await _libraryManager.DeleteItem(item, new DeleteOptions
  1379. {
  1380. DeleteFileLocation = true
  1381. }).ConfigureAwait(false);
  1382. }
  1383. catch (Exception ex)
  1384. {
  1385. _logger.ErrorException("Error deleting item", ex);
  1386. }
  1387. }
  1388. }
  1389. finally
  1390. {
  1391. _recordingDeleteSemaphore.Release();
  1392. }
  1393. }
  1394. private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
  1395. private async Task DeleteLibraryItemsForTimers(List<TimerInfo> timers)
  1396. {
  1397. foreach (var timer in timers)
  1398. {
  1399. if (_disposed)
  1400. {
  1401. return;
  1402. }
  1403. try
  1404. {
  1405. await DeleteLibraryItemForTimer(timer).ConfigureAwait(false);
  1406. }
  1407. catch (Exception ex)
  1408. {
  1409. _logger.ErrorException("Error deleting recording", ex);
  1410. }
  1411. }
  1412. }
  1413. private async Task DeleteLibraryItemForTimer(TimerInfo timer)
  1414. {
  1415. var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false);
  1416. if (libraryItem != null)
  1417. {
  1418. await _libraryManager.DeleteItem(libraryItem, new DeleteOptions
  1419. {
  1420. DeleteFileLocation = true
  1421. }).ConfigureAwait(false);
  1422. }
  1423. else
  1424. {
  1425. try
  1426. {
  1427. _fileSystem.DeleteFile(timer.RecordingPath);
  1428. }
  1429. catch (IOException)
  1430. {
  1431. }
  1432. }
  1433. _timerProvider.Delete(timer);
  1434. }
  1435. private string EnsureFileUnique(string path, string timerId)
  1436. {
  1437. var originalPath = path;
  1438. var index = 1;
  1439. while (FileExists(path, timerId))
  1440. {
  1441. var parent = _fileSystem.GetDirectoryName(originalPath);
  1442. var name = Path.GetFileNameWithoutExtension(originalPath);
  1443. name += "-" + index.ToString(CultureInfo.InvariantCulture);
  1444. path = Path.ChangeExtension(Path.Combine(parent, name), Path.GetExtension(originalPath));
  1445. index++;
  1446. }
  1447. return path;
  1448. }
  1449. private bool FileExists(string path, string timerId)
  1450. {
  1451. if (_fileSystem.FileExists(path))
  1452. {
  1453. return true;
  1454. }
  1455. var hasRecordingAtPath = _activeRecordings
  1456. .Values
  1457. .ToList()
  1458. .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
  1459. if (hasRecordingAtPath)
  1460. {
  1461. return true;
  1462. }
  1463. return false;
  1464. }
  1465. private async Task<IRecorder> GetRecorder()
  1466. {
  1467. var config = GetConfiguration();
  1468. var regInfo = await _liveTvManager.GetRegistrationInfo("embytvrecordingconversion").ConfigureAwait(false);
  1469. if (regInfo.IsValid)
  1470. {
  1471. if (config.EnableRecordingEncoding)
  1472. {
  1473. return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config, _httpClient, _processFactory, _config);
  1474. }
  1475. return new DirectRecorder(_logger, _httpClient, _fileSystem);
  1476. //var options = new LiveTvOptions
  1477. //{
  1478. // EnableOriginalAudioWithEncodedRecordings = true,
  1479. // RecordedVideoCodec = "copy",
  1480. // RecordingEncodingFormat = "ts"
  1481. //};
  1482. //return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, options, _httpClient, _processFactory, _config);
  1483. }
  1484. throw new InvalidOperationException("Emby DVR Requires an active Emby Premiere subscription.");
  1485. }
  1486. private async void OnSuccessfulRecording(TimerInfo timer, string path)
  1487. {
  1488. //if (timer.IsProgramSeries && GetConfiguration().EnableAutoOrganize)
  1489. //{
  1490. // try
  1491. // {
  1492. // // this is to account for the library monitor holding a lock for additional time after the change is complete.
  1493. // // ideally this shouldn't be hard-coded
  1494. // await Task.Delay(30000).ConfigureAwait(false);
  1495. // var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
  1496. // var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false);
  1497. // if (result.Status == FileSortingStatus.Success)
  1498. // {
  1499. // return;
  1500. // }
  1501. // }
  1502. // catch (Exception ex)
  1503. // {
  1504. // _logger.ErrorException("Error processing new recording", ex);
  1505. // }
  1506. //}
  1507. PostProcessRecording(timer, path);
  1508. }
  1509. private void PostProcessRecording(TimerInfo timer, string path)
  1510. {
  1511. var options = GetConfiguration();
  1512. if (string.IsNullOrWhiteSpace(options.RecordingPostProcessor))
  1513. {
  1514. return;
  1515. }
  1516. try
  1517. {
  1518. var process = _processFactory.Create(new ProcessOptions
  1519. {
  1520. Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
  1521. CreateNoWindow = true,
  1522. EnableRaisingEvents = true,
  1523. ErrorDialog = false,
  1524. FileName = options.RecordingPostProcessor,
  1525. IsHidden = true,
  1526. UseShellExecute = false
  1527. });
  1528. _logger.Info("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  1529. process.Exited += Process_Exited;
  1530. process.Start();
  1531. }
  1532. catch (Exception ex)
  1533. {
  1534. _logger.ErrorException("Error running recording post processor", ex);
  1535. }
  1536. }
  1537. private string GetPostProcessArguments(string path, string arguments)
  1538. {
  1539. return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
  1540. }
  1541. private void Process_Exited(object sender, EventArgs e)
  1542. {
  1543. var process = (IProcess)sender;
  1544. try
  1545. {
  1546. _logger.Info("Recording post-processing script completed with exit code {0}", process.ExitCode);
  1547. }
  1548. catch
  1549. {
  1550. }
  1551. process.Dispose();
  1552. }
  1553. private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
  1554. {
  1555. if (!image.IsLocalFile)
  1556. {
  1557. image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
  1558. }
  1559. string imageSaveFilenameWithoutExtension = null;
  1560. switch (image.Type)
  1561. {
  1562. case ImageType.Primary:
  1563. if (program.IsSeries)
  1564. {
  1565. imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
  1566. }
  1567. else
  1568. {
  1569. imageSaveFilenameWithoutExtension = "poster";
  1570. }
  1571. break;
  1572. case ImageType.Logo:
  1573. imageSaveFilenameWithoutExtension = "logo";
  1574. break;
  1575. case ImageType.Thumb:
  1576. imageSaveFilenameWithoutExtension = "landscape";
  1577. break;
  1578. case ImageType.Backdrop:
  1579. imageSaveFilenameWithoutExtension = "fanart";
  1580. break;
  1581. default:
  1582. break;
  1583. }
  1584. if (string.IsNullOrWhiteSpace(imageSaveFilenameWithoutExtension))
  1585. {
  1586. return;
  1587. }
  1588. var imageSavePath = Path.Combine(_fileSystem.GetDirectoryName(recordingPath), imageSaveFilenameWithoutExtension);
  1589. // preserve original image extension
  1590. imageSavePath = Path.ChangeExtension(imageSavePath, Path.GetExtension(image.Path));
  1591. _fileSystem.CopyFile(image.Path, imageSavePath, true);
  1592. }
  1593. private async Task SaveRecordingImages(string recordingPath, LiveTvProgram program)
  1594. {
  1595. var image = program.GetImageInfo(ImageType.Primary, 0);
  1596. if (image != null && program.IsMovie)
  1597. {
  1598. try
  1599. {
  1600. await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
  1601. }
  1602. catch (Exception ex)
  1603. {
  1604. _logger.ErrorException("Error saving recording image", ex);
  1605. }
  1606. }
  1607. if (!program.IsSeries)
  1608. {
  1609. image = program.GetImageInfo(ImageType.Backdrop, 0);
  1610. if (image != null)
  1611. {
  1612. try
  1613. {
  1614. await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
  1615. }
  1616. catch (Exception ex)
  1617. {
  1618. _logger.ErrorException("Error saving recording image", ex);
  1619. }
  1620. }
  1621. image = program.GetImageInfo(ImageType.Thumb, 0);
  1622. if (image != null)
  1623. {
  1624. try
  1625. {
  1626. await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
  1627. }
  1628. catch (Exception ex)
  1629. {
  1630. _logger.ErrorException("Error saving recording image", ex);
  1631. }
  1632. }
  1633. image = program.GetImageInfo(ImageType.Logo, 0);
  1634. if (image != null)
  1635. {
  1636. try
  1637. {
  1638. await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
  1639. }
  1640. catch (Exception ex)
  1641. {
  1642. _logger.ErrorException("Error saving recording image", ex);
  1643. }
  1644. }
  1645. }
  1646. }
  1647. private async void SaveRecordingMetadata(TimerInfo timer, string recordingPath, string seriesPath)
  1648. {
  1649. try
  1650. {
  1651. var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
  1652. {
  1653. IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
  1654. Limit = 1,
  1655. ExternalId = timer.ProgramId,
  1656. DtoOptions = new DtoOptions(true)
  1657. }).FirstOrDefault() as LiveTvProgram;
  1658. // dummy this up
  1659. if (program == null)
  1660. {
  1661. program = new LiveTvProgram
  1662. {
  1663. Name = timer.Name,
  1664. HomePageUrl = timer.HomePageUrl,
  1665. Overview = timer.Overview,
  1666. Genres = timer.Genres,
  1667. CommunityRating = timer.CommunityRating,
  1668. OfficialRating = timer.OfficialRating,
  1669. ProductionYear = timer.ProductionYear,
  1670. PremiereDate = timer.OriginalAirDate,
  1671. IndexNumber = timer.EpisodeNumber,
  1672. ParentIndexNumber = timer.SeasonNumber
  1673. };
  1674. }
  1675. if (timer.IsSports)
  1676. {
  1677. AddGenre(program.Genres, "Sports");
  1678. }
  1679. if (timer.IsKids)
  1680. {
  1681. AddGenre(program.Genres, "Kids");
  1682. AddGenre(program.Genres, "Children");
  1683. }
  1684. if (timer.IsNews)
  1685. {
  1686. AddGenre(program.Genres, "News");
  1687. }
  1688. if (timer.IsProgramSeries)
  1689. {
  1690. SaveSeriesNfo(timer, seriesPath);
  1691. SaveVideoNfo(timer, recordingPath, program, false);
  1692. }
  1693. else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
  1694. {
  1695. SaveVideoNfo(timer, recordingPath, program, true);
  1696. }
  1697. else
  1698. {
  1699. SaveVideoNfo(timer, recordingPath, program, false);
  1700. }
  1701. await SaveRecordingImages(recordingPath, program).ConfigureAwait(false);
  1702. }
  1703. catch (Exception ex)
  1704. {
  1705. _logger.ErrorException("Error saving nfo", ex);
  1706. }
  1707. }
  1708. private void SaveSeriesNfo(TimerInfo timer, string seriesPath)
  1709. {
  1710. var nfoPath = Path.Combine(seriesPath, "tvshow.nfo");
  1711. if (_fileSystem.FileExists(nfoPath))
  1712. {
  1713. return;
  1714. }
  1715. using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
  1716. {
  1717. var settings = new XmlWriterSettings
  1718. {
  1719. Indent = true,
  1720. Encoding = Encoding.UTF8,
  1721. CloseOutput = false
  1722. };
  1723. using (XmlWriter writer = XmlWriter.Create(stream, settings))
  1724. {
  1725. writer.WriteStartDocument(true);
  1726. writer.WriteStartElement("tvshow");
  1727. if (!string.IsNullOrWhiteSpace(timer.Name))
  1728. {
  1729. writer.WriteElementString("title", timer.Name);
  1730. }
  1731. if (!string.IsNullOrEmpty(timer.OfficialRating))
  1732. {
  1733. writer.WriteElementString("mpaa", timer.OfficialRating);
  1734. }
  1735. foreach (var genre in timer.Genres)
  1736. {
  1737. writer.WriteElementString("genre", genre);
  1738. }
  1739. writer.WriteEndElement();
  1740. writer.WriteEndDocument();
  1741. }
  1742. }
  1743. }
  1744. public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
  1745. private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData)
  1746. {
  1747. var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
  1748. if (_fileSystem.FileExists(nfoPath))
  1749. {
  1750. return;
  1751. }
  1752. using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
  1753. {
  1754. var settings = new XmlWriterSettings
  1755. {
  1756. Indent = true,
  1757. Encoding = Encoding.UTF8,
  1758. CloseOutput = false
  1759. };
  1760. var options = _config.GetNfoConfiguration();
  1761. using (XmlWriter writer = XmlWriter.Create(stream, settings))
  1762. {
  1763. writer.WriteStartDocument(true);
  1764. if (timer.IsProgramSeries)
  1765. {
  1766. writer.WriteStartElement("episodedetails");
  1767. if (!string.IsNullOrWhiteSpace(timer.EpisodeTitle))
  1768. {
  1769. writer.WriteElementString("title", timer.EpisodeTitle);
  1770. }
  1771. if (item.PremiereDate.HasValue)
  1772. {
  1773. var formatString = options.ReleaseDateFormat;
  1774. writer.WriteElementString("aired", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
  1775. }
  1776. if (item.IndexNumber.HasValue)
  1777. {
  1778. writer.WriteElementString("episode", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
  1779. }
  1780. if (item.ParentIndexNumber.HasValue)
  1781. {
  1782. writer.WriteElementString("season", item.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture));
  1783. }
  1784. }
  1785. else
  1786. {
  1787. writer.WriteStartElement("movie");
  1788. if (!string.IsNullOrWhiteSpace(item.Name))
  1789. {
  1790. writer.WriteElementString("title", item.Name);
  1791. }
  1792. if (!string.IsNullOrWhiteSpace(item.OriginalTitle))
  1793. {
  1794. writer.WriteElementString("originaltitle", item.OriginalTitle);
  1795. }
  1796. if (item.PremiereDate.HasValue)
  1797. {
  1798. var formatString = options.ReleaseDateFormat;
  1799. writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
  1800. writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
  1801. }
  1802. }
  1803. writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
  1804. if (item.ProductionYear.HasValue)
  1805. {
  1806. writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
  1807. }
  1808. if (!string.IsNullOrEmpty(item.OfficialRating))
  1809. {
  1810. writer.WriteElementString("mpaa", item.OfficialRating);
  1811. }
  1812. var overview = (item.Overview ?? string.Empty)
  1813. .StripHtml()
  1814. .Replace("&quot;", "'");
  1815. writer.WriteElementString("plot", overview);
  1816. if (lockData)
  1817. {
  1818. writer.WriteElementString("lockdata", true.ToString().ToLower());
  1819. }
  1820. if (item.CommunityRating.HasValue)
  1821. {
  1822. writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
  1823. }
  1824. foreach (var genre in item.Genres)
  1825. {
  1826. writer.WriteElementString("genre", genre);
  1827. }
  1828. if (!string.IsNullOrWhiteSpace(item.HomePageUrl))
  1829. {
  1830. writer.WriteElementString("website", item.HomePageUrl);
  1831. }
  1832. var people = item.Id == Guid.Empty ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
  1833. var directors = people
  1834. .Where(i => IsPersonType(i, PersonType.Director))
  1835. .Select(i => i.Name)
  1836. .ToList();
  1837. foreach (var person in directors)
  1838. {
  1839. writer.WriteElementString("director", person);
  1840. }
  1841. var writers = people
  1842. .Where(i => IsPersonType(i, PersonType.Writer))
  1843. .Select(i => i.Name)
  1844. .Distinct(StringComparer.OrdinalIgnoreCase)
  1845. .ToList();
  1846. foreach (var person in writers)
  1847. {
  1848. writer.WriteElementString("writer", person);
  1849. }
  1850. foreach (var person in writers)
  1851. {
  1852. writer.WriteElementString("credits", person);
  1853. }
  1854. var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
  1855. if (!string.IsNullOrEmpty(tmdbCollection))
  1856. {
  1857. writer.WriteElementString("collectionnumber", tmdbCollection);
  1858. }
  1859. var imdb = item.GetProviderId(MetadataProviders.Imdb);
  1860. if (!string.IsNullOrEmpty(imdb))
  1861. {
  1862. if (item is Series)
  1863. {
  1864. writer.WriteElementString("imdb_id", imdb);
  1865. }
  1866. else
  1867. {
  1868. writer.WriteElementString("imdbid", imdb);
  1869. }
  1870. }
  1871. var tvdb = item.GetProviderId(MetadataProviders.Tvdb);
  1872. if (!string.IsNullOrEmpty(tvdb))
  1873. {
  1874. writer.WriteElementString("tvdbid", tvdb);
  1875. }
  1876. var tmdb = item.GetProviderId(MetadataProviders.Tmdb);
  1877. if (!string.IsNullOrEmpty(tmdb))
  1878. {
  1879. writer.WriteElementString("tmdbid", tmdb);
  1880. }
  1881. if (item.CriticRating.HasValue)
  1882. {
  1883. writer.WriteElementString("criticrating", item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
  1884. }
  1885. if (!string.IsNullOrWhiteSpace(item.Tagline))
  1886. {
  1887. writer.WriteElementString("tagline", item.Tagline);
  1888. }
  1889. foreach (var studio in item.Studios)
  1890. {
  1891. writer.WriteElementString("studio", studio);
  1892. }
  1893. if (item.VoteCount.HasValue)
  1894. {
  1895. writer.WriteElementString("votes", item.VoteCount.Value.ToString(CultureInfo.InvariantCulture));
  1896. }
  1897. writer.WriteEndElement();
  1898. writer.WriteEndDocument();
  1899. }
  1900. }
  1901. }
  1902. private static bool IsPersonType(PersonInfo person, string type)
  1903. {
  1904. return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
  1905. }
  1906. private void AddGenre(List<string> genres, string genre)
  1907. {
  1908. if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
  1909. {
  1910. genres.Add(genre);
  1911. }
  1912. }
  1913. private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
  1914. {
  1915. var epgData = GetEpgDataForChannel(channelId);
  1916. return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
  1917. }
  1918. private ProgramInfo GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
  1919. {
  1920. var epgData = GetEpgDataForChannel(channelId);
  1921. var startDateTicks = startDateUtc.Ticks;
  1922. // Find the first program that starts within 3 minutes
  1923. return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
  1924. }
  1925. private LiveTvOptions GetConfiguration()
  1926. {
  1927. return _config.GetConfiguration<LiveTvOptions>("livetv");
  1928. }
  1929. private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
  1930. {
  1931. if (timer.IsManual)
  1932. {
  1933. return false;
  1934. }
  1935. if (!seriesTimer.RecordAnyTime)
  1936. {
  1937. if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks)
  1938. {
  1939. return true;
  1940. }
  1941. }
  1942. //if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
  1943. //{
  1944. // return true;
  1945. //}
  1946. if (seriesTimer.RecordNewOnly && timer.IsRepeat)
  1947. {
  1948. return true;
  1949. }
  1950. if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
  1951. {
  1952. return true;
  1953. }
  1954. return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
  1955. }
  1956. private void HandleDuplicateShowIds(List<TimerInfo> timers)
  1957. {
  1958. foreach (var timer in timers.Skip(1))
  1959. {
  1960. // TODO: Get smarter, prefer HD, etc
  1961. timer.Status = RecordingStatus.Cancelled;
  1962. _timerProvider.Update(timer);
  1963. }
  1964. }
  1965. private void SearchForDuplicateShowIds(List<TimerInfo> timers)
  1966. {
  1967. var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList();
  1968. foreach (var group in groups)
  1969. {
  1970. if (string.IsNullOrWhiteSpace(group.Key))
  1971. {
  1972. continue;
  1973. }
  1974. var groupTimers = group.ToList();
  1975. if (groupTimers.Count < 2)
  1976. {
  1977. continue;
  1978. }
  1979. HandleDuplicateShowIds(groupTimers);
  1980. }
  1981. }
  1982. private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool updateTimerSettings, bool deleteInvalidTimers)
  1983. {
  1984. var allTimers = GetTimersForSeries(seriesTimer, epgData)
  1985. .ToList();
  1986. var registration = await _liveTvManager.GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
  1987. var enabledTimersForSeries = new List<TimerInfo>();
  1988. if (registration.IsValid)
  1989. {
  1990. foreach (var timer in allTimers)
  1991. {
  1992. var existingTimer = _timerProvider.GetTimer(timer.Id);
  1993. if (existingTimer == null)
  1994. {
  1995. existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
  1996. ? null
  1997. : _timerProvider.GetTimerByProgramId(timer.ProgramId);
  1998. }
  1999. if (existingTimer == null)
  2000. {
  2001. if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
  2002. {
  2003. timer.Status = RecordingStatus.Cancelled;
  2004. }
  2005. else
  2006. {
  2007. enabledTimersForSeries.Add(timer);
  2008. }
  2009. _timerProvider.Add(timer);
  2010. }
  2011. else
  2012. {
  2013. // Only update if not currently active - test both new timer and existing in case Id's are different
  2014. // Id's could be different if the timer was created manually prior to series timer creation
  2015. ActiveRecordingInfo activeRecordingInfo;
  2016. if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo) && !_activeRecordings.TryGetValue(existingTimer.Id, out activeRecordingInfo))
  2017. {
  2018. UpdateExistingTimerWithNewMetadata(existingTimer, timer);
  2019. // Needed by ShouldCancelTimerForSeriesTimer
  2020. timer.IsManual = existingTimer.IsManual;
  2021. if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
  2022. {
  2023. existingTimer.Status = RecordingStatus.Cancelled;
  2024. }
  2025. if (existingTimer.Status != RecordingStatus.Cancelled)
  2026. {
  2027. enabledTimersForSeries.Add(existingTimer);
  2028. }
  2029. if (updateTimerSettings)
  2030. {
  2031. existingTimer.KeepUntil = seriesTimer.KeepUntil;
  2032. existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
  2033. existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
  2034. existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
  2035. existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
  2036. existingTimer.Priority = seriesTimer.Priority;
  2037. }
  2038. existingTimer.SeriesTimerId = seriesTimer.Id;
  2039. _timerProvider.Update(existingTimer);
  2040. }
  2041. }
  2042. }
  2043. }
  2044. SearchForDuplicateShowIds(enabledTimersForSeries);
  2045. if (deleteInvalidTimers)
  2046. {
  2047. var allTimerIds = allTimers
  2048. .Select(i => i.Id)
  2049. .ToList();
  2050. var deleteStatuses = new List<RecordingStatus>
  2051. {
  2052. RecordingStatus.New
  2053. };
  2054. var deletes = _timerProvider.GetAll()
  2055. .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
  2056. .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
  2057. .Where(i => deleteStatuses.Contains(i.Status))
  2058. .ToList();
  2059. foreach (var timer in deletes)
  2060. {
  2061. CancelTimerInternal(timer.Id, false);
  2062. }
  2063. }
  2064. }
  2065. private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
  2066. {
  2067. if (seriesTimer == null)
  2068. {
  2069. throw new ArgumentNullException("seriesTimer");
  2070. }
  2071. if (allPrograms == null)
  2072. {
  2073. throw new ArgumentNullException("allPrograms");
  2074. }
  2075. // Exclude programs that have already ended
  2076. allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow);
  2077. allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
  2078. return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
  2079. }
  2080. private bool IsProgramAlreadyInLibrary(TimerInfo program)
  2081. {
  2082. if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
  2083. {
  2084. var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
  2085. {
  2086. IncludeItemTypes = new[] { typeof(Series).Name },
  2087. Name = program.Name
  2088. }).Select(i => i.ToString("N")).ToArray();
  2089. if (seriesIds.Length == 0)
  2090. {
  2091. return false;
  2092. }
  2093. if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
  2094. {
  2095. var result = _libraryManager.GetItemIds(new InternalItemsQuery
  2096. {
  2097. IncludeItemTypes = new[] { typeof(Episode).Name },
  2098. ParentIndexNumber = program.SeasonNumber.Value,
  2099. IndexNumber = program.EpisodeNumber.Value,
  2100. AncestorIds = seriesIds,
  2101. IsVirtualItem = false,
  2102. Limit = 1
  2103. });
  2104. if (result.Count > 0)
  2105. {
  2106. return true;
  2107. }
  2108. }
  2109. }
  2110. return false;
  2111. }
  2112. private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
  2113. {
  2114. if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
  2115. {
  2116. _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
  2117. return new List<ProgramInfo>();
  2118. }
  2119. return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
  2120. }
  2121. private string GetChannelEpgCachePath(string channelId)
  2122. {
  2123. return Path.Combine(_config.CommonApplicationPaths.CachePath, "embytvepg", channelId + ".json");
  2124. }
  2125. private readonly object _epgLock = new object();
  2126. private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
  2127. {
  2128. var path = GetChannelEpgCachePath(channelId);
  2129. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
  2130. lock (_epgLock)
  2131. {
  2132. _jsonSerializer.SerializeToFile(epgData, path);
  2133. }
  2134. }
  2135. private List<ProgramInfo> GetEpgDataForChannel(string channelId)
  2136. {
  2137. try
  2138. {
  2139. lock (_epgLock)
  2140. {
  2141. return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
  2142. }
  2143. }
  2144. catch
  2145. {
  2146. return new List<ProgramInfo>();
  2147. }
  2148. }
  2149. private List<ProgramInfo> GetEpgDataForChannels(List<string> channelIds)
  2150. {
  2151. return channelIds.SelectMany(GetEpgDataForChannel).ToList();
  2152. }
  2153. private bool _disposed;
  2154. public void Dispose()
  2155. {
  2156. _disposed = true;
  2157. foreach (var pair in _activeRecordings.ToList())
  2158. {
  2159. pair.Value.CancellationTokenSource.Cancel();
  2160. }
  2161. }
  2162. public List<VirtualFolderInfo> GetRecordingFolders()
  2163. {
  2164. var list = new List<VirtualFolderInfo>();
  2165. var defaultFolder = RecordingPath;
  2166. var defaultName = "Recordings";
  2167. if (_fileSystem.DirectoryExists(defaultFolder))
  2168. {
  2169. list.Add(new VirtualFolderInfo
  2170. {
  2171. Locations = new List<string> { defaultFolder },
  2172. Name = defaultName
  2173. });
  2174. }
  2175. var customPath = GetConfiguration().MovieRecordingPath;
  2176. if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && _fileSystem.DirectoryExists(customPath))
  2177. {
  2178. list.Add(new VirtualFolderInfo
  2179. {
  2180. Locations = new List<string> { customPath },
  2181. Name = "Recorded Movies",
  2182. CollectionType = CollectionType.Movies
  2183. });
  2184. }
  2185. customPath = GetConfiguration().SeriesRecordingPath;
  2186. if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && _fileSystem.DirectoryExists(customPath))
  2187. {
  2188. list.Add(new VirtualFolderInfo
  2189. {
  2190. Locations = new List<string> { customPath },
  2191. Name = "Recorded Shows",
  2192. CollectionType = CollectionType.TvShows
  2193. });
  2194. }
  2195. return list;
  2196. }
  2197. class ActiveRecordingInfo
  2198. {
  2199. public string Path { get; set; }
  2200. public TimerInfo Timer { get; set; }
  2201. public ProgramInfo Program { get; set; }
  2202. public CancellationTokenSource CancellationTokenSource { get; set; }
  2203. }
  2204. private const int TunerDiscoveryDurationMs = 3000;
  2205. public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
  2206. {
  2207. var list = new List<TunerHostInfo>();
  2208. var configuredDeviceIds = GetConfiguration().TunerHosts
  2209. .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
  2210. .Select(i => i.DeviceId)
  2211. .ToList();
  2212. foreach (var host in _liveTvManager.TunerHosts)
  2213. {
  2214. var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
  2215. if (newDevicesOnly)
  2216. {
  2217. discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
  2218. .ToList();
  2219. }
  2220. list.AddRange(discoveredDevices);
  2221. }
  2222. return list;
  2223. }
  2224. public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
  2225. {
  2226. foreach (var host in _liveTvManager.TunerHosts)
  2227. {
  2228. await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
  2229. }
  2230. }
  2231. private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
  2232. {
  2233. var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
  2234. var configuredDevices = GetConfiguration().TunerHosts
  2235. .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
  2236. .ToList();
  2237. foreach (var device in discoveredDevices)
  2238. {
  2239. var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
  2240. if (configuredDevice != null)
  2241. {
  2242. if (!string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
  2243. {
  2244. _logger.Info("Tuner url has changed from {0} to {1}", configuredDevice.Url, device.Url);
  2245. configuredDevice.Url = device.Url;
  2246. await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false);
  2247. }
  2248. }
  2249. }
  2250. }
  2251. private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken)
  2252. {
  2253. try
  2254. {
  2255. var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false);
  2256. foreach (var device in discoveredDevices)
  2257. {
  2258. _logger.Info("Discovered tuner device {0} at {1}", host.Name, device.Url);
  2259. }
  2260. return discoveredDevices;
  2261. }
  2262. catch (Exception ex)
  2263. {
  2264. _logger.ErrorException("Error discovering tuner devices", ex);
  2265. return new List<TunerHostInfo>();
  2266. }
  2267. }
  2268. }
  2269. public static class ConfigurationExtension
  2270. {
  2271. public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager)
  2272. {
  2273. return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
  2274. }
  2275. }
  2276. }