EmbyTV.cs 104 KB

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