EmbyTV.cs 105 KB

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