ChannelManager.cs 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Jellyfin.Data.Entities;
  9. using Jellyfin.Data.Enums;
  10. using MediaBrowser.Common.Extensions;
  11. using MediaBrowser.Common.Progress;
  12. using MediaBrowser.Controller.Channels;
  13. using MediaBrowser.Controller.Configuration;
  14. using MediaBrowser.Controller.Dto;
  15. using MediaBrowser.Controller.Entities;
  16. using MediaBrowser.Controller.Entities.Audio;
  17. using MediaBrowser.Controller.Library;
  18. using MediaBrowser.Controller.Providers;
  19. using MediaBrowser.Model.Channels;
  20. using MediaBrowser.Model.Dto;
  21. using MediaBrowser.Model.Entities;
  22. using MediaBrowser.Model.IO;
  23. using MediaBrowser.Model.Querying;
  24. using MediaBrowser.Model.Serialization;
  25. using Microsoft.Extensions.Caching.Memory;
  26. using Microsoft.Extensions.Logging;
  27. using Episode = MediaBrowser.Controller.Entities.TV.Episode;
  28. using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
  29. using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
  30. using Season = MediaBrowser.Controller.Entities.TV.Season;
  31. using Series = MediaBrowser.Controller.Entities.TV.Series;
  32. namespace Emby.Server.Implementations.Channels
  33. {
  34. /// <summary>
  35. /// The LiveTV channel manager.
  36. /// </summary>
  37. public class ChannelManager : IChannelManager
  38. {
  39. private readonly IUserManager _userManager;
  40. private readonly IUserDataManager _userDataManager;
  41. private readonly IDtoService _dtoService;
  42. private readonly ILibraryManager _libraryManager;
  43. private readonly ILogger<ChannelManager> _logger;
  44. private readonly IServerConfigurationManager _config;
  45. private readonly IFileSystem _fileSystem;
  46. private readonly IJsonSerializer _jsonSerializer;
  47. private readonly IProviderManager _providerManager;
  48. private readonly IMemoryCache _memoryCache;
  49. private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
  50. /// <summary>
  51. /// Initializes a new instance of the <see cref="ChannelManager"/> class.
  52. /// </summary>
  53. /// <param name="userManager">The user manager.</param>
  54. /// <param name="dtoService">The dto service.</param>
  55. /// <param name="libraryManager">The library manager.</param>
  56. /// <param name="logger">The logger.</param>
  57. /// <param name="config">The server configuration manager.</param>
  58. /// <param name="fileSystem">The filesystem.</param>
  59. /// <param name="userDataManager">The user data manager.</param>
  60. /// <param name="jsonSerializer">The JSON serializer.</param>
  61. /// <param name="providerManager">The provider manager.</param>
  62. /// <param name="memoryCache">The memory cache.</param>
  63. public ChannelManager(
  64. IUserManager userManager,
  65. IDtoService dtoService,
  66. ILibraryManager libraryManager,
  67. ILogger<ChannelManager> logger,
  68. IServerConfigurationManager config,
  69. IFileSystem fileSystem,
  70. IUserDataManager userDataManager,
  71. IJsonSerializer jsonSerializer,
  72. IProviderManager providerManager,
  73. IMemoryCache memoryCache)
  74. {
  75. _userManager = userManager;
  76. _dtoService = dtoService;
  77. _libraryManager = libraryManager;
  78. _logger = logger;
  79. _config = config;
  80. _fileSystem = fileSystem;
  81. _userDataManager = userDataManager;
  82. _jsonSerializer = jsonSerializer;
  83. _providerManager = providerManager;
  84. _memoryCache = memoryCache;
  85. }
  86. internal IChannel[] Channels { get; private set; }
  87. private static TimeSpan CacheLength => TimeSpan.FromHours(3);
  88. /// <inheritdoc />
  89. public void AddParts(IEnumerable<IChannel> channels)
  90. {
  91. Channels = channels.ToArray();
  92. }
  93. /// <inheritdoc />
  94. public bool EnableMediaSourceDisplay(BaseItem item)
  95. {
  96. var internalChannel = _libraryManager.GetItemById(item.ChannelId);
  97. var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
  98. return !(channel is IDisableMediaSourceDisplay);
  99. }
  100. /// <inheritdoc />
  101. public bool CanDelete(BaseItem item)
  102. {
  103. var internalChannel = _libraryManager.GetItemById(item.ChannelId);
  104. var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
  105. return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item);
  106. }
  107. /// <inheritdoc />
  108. public bool EnableMediaProbe(BaseItem item)
  109. {
  110. var internalChannel = _libraryManager.GetItemById(item.ChannelId);
  111. var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
  112. return channel is ISupportsMediaProbe;
  113. }
  114. /// <inheritdoc />
  115. public Task DeleteItem(BaseItem item)
  116. {
  117. var internalChannel = _libraryManager.GetItemById(item.ChannelId);
  118. if (internalChannel == null)
  119. {
  120. throw new ArgumentException();
  121. }
  122. var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
  123. var supportsDelete = channel as ISupportsDelete;
  124. if (supportsDelete == null)
  125. {
  126. throw new ArgumentException();
  127. }
  128. return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
  129. }
  130. private IEnumerable<IChannel> GetAllChannels()
  131. {
  132. return Channels
  133. .OrderBy(i => i.Name);
  134. }
  135. /// <summary>
  136. /// Get the installed channel IDs.
  137. /// </summary>
  138. /// <returns>An <see cref="IEnumerable{T}"/> containing installed channel IDs.</returns>
  139. public IEnumerable<Guid> GetInstalledChannelIds()
  140. {
  141. return GetAllChannels().Select(i => GetInternalChannelId(i.Name));
  142. }
  143. /// <inheritdoc />
  144. public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
  145. {
  146. var user = query.UserId.Equals(Guid.Empty)
  147. ? null
  148. : _userManager.GetUserById(query.UserId);
  149. var channels = GetAllChannels()
  150. .Select(GetChannelEntity)
  151. .OrderBy(i => i.SortName)
  152. .ToList();
  153. if (query.IsRecordingsFolder.HasValue)
  154. {
  155. var val = query.IsRecordingsFolder.Value;
  156. channels = channels.Where(i =>
  157. {
  158. try
  159. {
  160. return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
  161. && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
  162. }
  163. catch
  164. {
  165. return false;
  166. }
  167. }).ToList();
  168. }
  169. if (query.SupportsLatestItems.HasValue)
  170. {
  171. var val = query.SupportsLatestItems.Value;
  172. channels = channels.Where(i =>
  173. {
  174. try
  175. {
  176. return GetChannelProvider(i) is ISupportsLatestMedia == val;
  177. }
  178. catch
  179. {
  180. return false;
  181. }
  182. }).ToList();
  183. }
  184. if (query.SupportsMediaDeletion.HasValue)
  185. {
  186. var val = query.SupportsMediaDeletion.Value;
  187. channels = channels.Where(i =>
  188. {
  189. try
  190. {
  191. return GetChannelProvider(i) is ISupportsDelete == val;
  192. }
  193. catch
  194. {
  195. return false;
  196. }
  197. }).ToList();
  198. }
  199. if (query.IsFavorite.HasValue)
  200. {
  201. var val = query.IsFavorite.Value;
  202. channels = channels.Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val)
  203. .ToList();
  204. }
  205. if (user != null)
  206. {
  207. channels = channels.Where(i =>
  208. {
  209. if (!i.IsVisible(user))
  210. {
  211. return false;
  212. }
  213. try
  214. {
  215. return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
  216. }
  217. catch
  218. {
  219. return false;
  220. }
  221. }).ToList();
  222. }
  223. var all = channels;
  224. var totalCount = all.Count;
  225. if (query.StartIndex.HasValue)
  226. {
  227. all = all.Skip(query.StartIndex.Value).ToList();
  228. }
  229. if (query.Limit.HasValue)
  230. {
  231. all = all.Take(query.Limit.Value).ToList();
  232. }
  233. var returnItems = all.ToArray();
  234. if (query.RefreshLatestChannelItems)
  235. {
  236. foreach (var item in returnItems)
  237. {
  238. RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
  239. }
  240. }
  241. return new QueryResult<Channel>
  242. {
  243. Items = returnItems,
  244. TotalRecordCount = totalCount
  245. };
  246. }
  247. /// <inheritdoc />
  248. public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
  249. {
  250. var user = query.UserId.Equals(Guid.Empty)
  251. ? null
  252. : _userManager.GetUserById(query.UserId);
  253. var internalResult = GetChannelsInternal(query);
  254. var dtoOptions = new DtoOptions();
  255. // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
  256. var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
  257. var result = new QueryResult<BaseItemDto>
  258. {
  259. Items = returnItems,
  260. TotalRecordCount = internalResult.TotalRecordCount
  261. };
  262. return result;
  263. }
  264. /// <summary>
  265. /// Refreshes the associated channels.
  266. /// </summary>
  267. /// <param name="progress">The progress.</param>
  268. /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
  269. /// <returns>The completed task.</returns>
  270. public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken)
  271. {
  272. var allChannelsList = GetAllChannels().ToList();
  273. var numComplete = 0;
  274. foreach (var channelInfo in allChannelsList)
  275. {
  276. cancellationToken.ThrowIfCancellationRequested();
  277. try
  278. {
  279. await GetChannel(channelInfo, cancellationToken).ConfigureAwait(false);
  280. }
  281. catch (OperationCanceledException)
  282. {
  283. throw;
  284. }
  285. catch (Exception ex)
  286. {
  287. _logger.LogError(ex, "Error getting channel information for {0}", channelInfo.Name);
  288. }
  289. numComplete++;
  290. double percent = (double)numComplete / allChannelsList.Count;
  291. progress.Report(100 * percent);
  292. }
  293. progress.Report(100);
  294. }
  295. private Channel GetChannelEntity(IChannel channel)
  296. {
  297. return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
  298. }
  299. private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item)
  300. {
  301. var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
  302. try
  303. {
  304. return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>();
  305. }
  306. catch
  307. {
  308. return new List<MediaSourceInfo>();
  309. }
  310. }
  311. private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
  312. {
  313. var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
  314. if (mediaSources == null || mediaSources.Count == 0)
  315. {
  316. try
  317. {
  318. _fileSystem.DeleteFile(path);
  319. }
  320. catch
  321. {
  322. }
  323. return;
  324. }
  325. Directory.CreateDirectory(Path.GetDirectoryName(path));
  326. _jsonSerializer.SerializeToFile(mediaSources, path);
  327. }
  328. /// <inheritdoc />
  329. public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken)
  330. {
  331. IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item);
  332. return results
  333. .Select(i => NormalizeMediaSource(item, i))
  334. .ToList();
  335. }
  336. /// <summary>
  337. /// Gets the dynamic media sources based on the provided item.
  338. /// </summary>
  339. /// <param name="item">The item.</param>
  340. /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
  341. /// <returns>The task representing the operation to get the media sources.</returns>
  342. public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken)
  343. {
  344. var channel = GetChannel(item.ChannelId);
  345. var channelPlugin = GetChannelProvider(channel);
  346. IEnumerable<MediaSourceInfo> results;
  347. if (channelPlugin is IRequiresMediaInfoCallback requiresCallback)
  348. {
  349. results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
  350. .ConfigureAwait(false);
  351. }
  352. else
  353. {
  354. results = new List<MediaSourceInfo>();
  355. }
  356. return results
  357. .Select(i => NormalizeMediaSource(item, i))
  358. .ToList();
  359. }
  360. private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
  361. {
  362. if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
  363. {
  364. return cachedInfo;
  365. }
  366. var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
  367. .ConfigureAwait(false);
  368. var list = mediaInfo.ToList();
  369. _memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
  370. return list;
  371. }
  372. private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info)
  373. {
  374. info.RunTimeTicks ??= item.RunTimeTicks;
  375. return info;
  376. }
  377. private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken)
  378. {
  379. var parentFolderId = Guid.Empty;
  380. var id = GetInternalChannelId(channelInfo.Name);
  381. var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id);
  382. var isNew = false;
  383. var forceUpdate = false;
  384. var item = _libraryManager.GetItemById(id) as Channel;
  385. if (item == null)
  386. {
  387. item = new Channel
  388. {
  389. Name = channelInfo.Name,
  390. Id = id,
  391. DateCreated = _fileSystem.GetCreationTimeUtc(path),
  392. DateModified = _fileSystem.GetLastWriteTimeUtc(path)
  393. };
  394. isNew = true;
  395. }
  396. if (!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
  397. {
  398. isNew = true;
  399. }
  400. item.Path = path;
  401. if (!item.ChannelId.Equals(id))
  402. {
  403. forceUpdate = true;
  404. }
  405. item.ChannelId = id;
  406. if (item.ParentId != parentFolderId)
  407. {
  408. forceUpdate = true;
  409. }
  410. item.ParentId = parentFolderId;
  411. item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
  412. item.Overview = channelInfo.Description;
  413. if (string.IsNullOrWhiteSpace(item.Name))
  414. {
  415. item.Name = channelInfo.Name;
  416. }
  417. if (isNew)
  418. {
  419. item.OnMetadataChanged();
  420. _libraryManager.CreateItem(item, null);
  421. }
  422. await item.RefreshMetadata(
  423. new MetadataRefreshOptions(new DirectoryService(_fileSystem))
  424. {
  425. ForceSave = !isNew && forceUpdate
  426. },
  427. cancellationToken).ConfigureAwait(false);
  428. return item;
  429. }
  430. private static string GetOfficialRating(ChannelParentalRating rating)
  431. {
  432. return rating switch
  433. {
  434. ChannelParentalRating.Adult => "XXX",
  435. ChannelParentalRating.UsR => "R",
  436. ChannelParentalRating.UsPG13 => "PG-13",
  437. ChannelParentalRating.UsPG => "PG",
  438. _ => null
  439. };
  440. }
  441. /// <summary>
  442. /// Gets a channel with the provided Guid.
  443. /// </summary>
  444. /// <param name="id">The Guid.</param>
  445. /// <returns>The corresponding channel.</returns>
  446. public Channel GetChannel(Guid id)
  447. {
  448. return _libraryManager.GetItemById(id) as Channel;
  449. }
  450. /// <inheritdoc />
  451. public Channel GetChannel(string id)
  452. {
  453. return _libraryManager.GetItemById(id) as Channel;
  454. }
  455. /// <inheritdoc />
  456. public ChannelFeatures[] GetAllChannelFeatures()
  457. {
  458. return _libraryManager.GetItemIds(
  459. new InternalItemsQuery
  460. {
  461. IncludeItemTypes = new[] { typeof(Channel).Name },
  462. OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
  463. }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
  464. }
  465. /// <inheritdoc />
  466. public ChannelFeatures GetChannelFeatures(string id)
  467. {
  468. if (string.IsNullOrEmpty(id))
  469. {
  470. throw new ArgumentNullException(nameof(id));
  471. }
  472. var channel = GetChannel(id);
  473. var channelProvider = GetChannelProvider(channel);
  474. return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures());
  475. }
  476. /// <summary>
  477. /// Checks whether the provided Guid supports external transfer.
  478. /// </summary>
  479. /// <param name="channelId">The Guid.</param>
  480. /// <returns>Whether or not the provided Guid supports external transfer.</returns>
  481. public bool SupportsExternalTransfer(Guid channelId)
  482. {
  483. var channelProvider = GetChannelProvider(channelId);
  484. return channelProvider.GetChannelFeatures().SupportsContentDownloading;
  485. }
  486. /// <summary>
  487. /// Gets the provided channel's supported features.
  488. /// </summary>
  489. /// <param name="channel">The channel.</param>
  490. /// <param name="provider">The provider.</param>
  491. /// <param name="features">The features.</param>
  492. /// <returns>The supported features.</returns>
  493. public ChannelFeatures GetChannelFeaturesDto(
  494. Channel channel,
  495. IChannel provider,
  496. InternalChannelFeatures features)
  497. {
  498. var supportsLatest = provider is ISupportsLatestMedia;
  499. return new ChannelFeatures
  500. {
  501. CanFilter = !features.MaxPageSize.HasValue,
  502. CanSearch = provider is ISearchableChannel,
  503. ContentTypes = features.ContentTypes.ToArray(),
  504. DefaultSortFields = features.DefaultSortFields.ToArray(),
  505. MaxPageSize = features.MaxPageSize,
  506. MediaTypes = features.MediaTypes.ToArray(),
  507. SupportsSortOrderToggle = features.SupportsSortOrderToggle,
  508. SupportsLatestMedia = supportsLatest,
  509. Name = channel.Name,
  510. Id = channel.Id.ToString("N", CultureInfo.InvariantCulture),
  511. SupportsContentDownloading = features.SupportsContentDownloading,
  512. AutoRefreshLevels = features.AutoRefreshLevels
  513. };
  514. }
  515. private Guid GetInternalChannelId(string name)
  516. {
  517. if (string.IsNullOrEmpty(name))
  518. {
  519. throw new ArgumentNullException(nameof(name));
  520. }
  521. return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
  522. }
  523. /// <inheritdoc />
  524. public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken)
  525. {
  526. var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false);
  527. var items = internalResult.Items;
  528. var totalRecordCount = internalResult.TotalRecordCount;
  529. var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User);
  530. var result = new QueryResult<BaseItemDto>
  531. {
  532. Items = returnItems,
  533. TotalRecordCount = totalRecordCount
  534. };
  535. return result;
  536. }
  537. /// <inheritdoc />
  538. public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken)
  539. {
  540. var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
  541. if (query.ChannelIds.Length > 0)
  542. {
  543. // Avoid implicitly captured closure
  544. var ids = query.ChannelIds;
  545. channels = channels
  546. .Where(i => ids.Contains(GetInternalChannelId(i.Name)))
  547. .ToArray();
  548. }
  549. if (channels.Length == 0)
  550. {
  551. return new QueryResult<BaseItem>();
  552. }
  553. foreach (var channel in channels)
  554. {
  555. await RefreshLatestChannelItems(channel, cancellationToken).ConfigureAwait(false);
  556. }
  557. query.IsFolder = false;
  558. // hack for trailers, figure out a better way later
  559. var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal);
  560. if (sortByPremiereDate)
  561. {
  562. query.OrderBy = new[]
  563. {
  564. (ItemSortBy.PremiereDate, SortOrder.Descending),
  565. (ItemSortBy.ProductionYear, SortOrder.Descending),
  566. (ItemSortBy.DateCreated, SortOrder.Descending)
  567. };
  568. }
  569. else
  570. {
  571. query.OrderBy = new[]
  572. {
  573. (ItemSortBy.DateCreated, SortOrder.Descending)
  574. };
  575. }
  576. return _libraryManager.GetItemsResult(query);
  577. }
  578. private async Task RefreshLatestChannelItems(IChannel channel, CancellationToken cancellationToken)
  579. {
  580. var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
  581. var query = new InternalItemsQuery
  582. {
  583. Parent = internalChannel,
  584. EnableTotalRecordCount = false,
  585. ChannelIds = new Guid[] { internalChannel.Id }
  586. };
  587. var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
  588. foreach (var item in result.Items)
  589. {
  590. if (item is Folder folder)
  591. {
  592. await GetChannelItemsInternal(
  593. new InternalItemsQuery
  594. {
  595. Parent = folder,
  596. EnableTotalRecordCount = false,
  597. ChannelIds = new Guid[] { internalChannel.Id }
  598. },
  599. new SimpleProgress<double>(),
  600. cancellationToken).ConfigureAwait(false);
  601. }
  602. }
  603. }
  604. /// <inheritdoc />
  605. public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken)
  606. {
  607. // Get the internal channel entity
  608. var channel = GetChannel(query.ChannelIds[0]);
  609. // Find the corresponding channel provider plugin
  610. var channelProvider = GetChannelProvider(channel);
  611. var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId);
  612. var itemsResult = await GetChannelItems(
  613. channelProvider,
  614. query.User,
  615. parentItem is Channel ? null : parentItem.ExternalId,
  616. null,
  617. false,
  618. cancellationToken)
  619. .ConfigureAwait(false);
  620. if (query.ParentId == Guid.Empty)
  621. {
  622. query.Parent = channel;
  623. }
  624. query.ChannelIds = Array.Empty<Guid>();
  625. // Not yet sure why this is causing a problem
  626. query.GroupByPresentationUniqueKey = false;
  627. // null if came from cache
  628. if (itemsResult != null)
  629. {
  630. var internalItems = itemsResult.Items
  631. .Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken))
  632. .ToArray();
  633. var existingIds = _libraryManager.GetItemIds(query);
  634. var deadIds = existingIds.Except(internalItems.Select(i => i.Id))
  635. .ToArray();
  636. foreach (var deadId in deadIds)
  637. {
  638. var deadItem = _libraryManager.GetItemById(deadId);
  639. if (deadItem != null)
  640. {
  641. _libraryManager.DeleteItem(
  642. deadItem,
  643. new DeleteOptions
  644. {
  645. DeleteFileLocation = false,
  646. DeleteFromExternalProvider = false
  647. },
  648. parentItem,
  649. false);
  650. }
  651. }
  652. }
  653. return _libraryManager.GetItemsResult(query);
  654. }
  655. /// <inheritdoc />
  656. public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken)
  657. {
  658. var internalResult = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
  659. var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User);
  660. var result = new QueryResult<BaseItemDto>
  661. {
  662. Items = returnItems,
  663. TotalRecordCount = internalResult.TotalRecordCount
  664. };
  665. return result;
  666. }
  667. private async Task<ChannelItemResult> GetChannelItems(
  668. IChannel channel,
  669. User user,
  670. string externalFolderId,
  671. ChannelItemSortField? sortField,
  672. bool sortDescending,
  673. CancellationToken cancellationToken)
  674. {
  675. var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
  676. var cacheLength = CacheLength;
  677. var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
  678. try
  679. {
  680. if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
  681. {
  682. var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
  683. if (cachedResult != null)
  684. {
  685. return null;
  686. }
  687. }
  688. }
  689. catch (FileNotFoundException)
  690. {
  691. }
  692. catch (IOException)
  693. {
  694. }
  695. await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  696. try
  697. {
  698. try
  699. {
  700. if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
  701. {
  702. var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
  703. if (cachedResult != null)
  704. {
  705. return null;
  706. }
  707. }
  708. }
  709. catch (FileNotFoundException)
  710. {
  711. }
  712. catch (IOException)
  713. {
  714. }
  715. var query = new InternalChannelItemQuery
  716. {
  717. UserId = user?.Id ?? Guid.Empty,
  718. SortBy = sortField,
  719. SortDescending = sortDescending,
  720. FolderId = externalFolderId
  721. };
  722. query.FolderId = externalFolderId;
  723. var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false);
  724. if (result == null)
  725. {
  726. throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
  727. }
  728. CacheResponse(result, cachePath);
  729. return result;
  730. }
  731. finally
  732. {
  733. _resourcePool.Release();
  734. }
  735. }
  736. private void CacheResponse(object result, string path)
  737. {
  738. try
  739. {
  740. Directory.CreateDirectory(Path.GetDirectoryName(path));
  741. _jsonSerializer.SerializeToFile(result, path);
  742. }
  743. catch (Exception ex)
  744. {
  745. _logger.LogError(ex, "Error writing to channel cache file: {path}", path);
  746. }
  747. }
  748. private string GetChannelDataCachePath(
  749. IChannel channel,
  750. string userId,
  751. string externalFolderId,
  752. ChannelItemSortField? sortField,
  753. bool sortDescending)
  754. {
  755. var channelId = GetInternalChannelId(channel.Name).ToString("N", CultureInfo.InvariantCulture);
  756. var userCacheKey = string.Empty;
  757. if (channel is IHasCacheKey hasCacheKey)
  758. {
  759. userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty;
  760. }
  761. var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N", CultureInfo.InvariantCulture);
  762. filename += userCacheKey;
  763. var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N", CultureInfo.InvariantCulture);
  764. if (sortField.HasValue)
  765. {
  766. filename += "-sortField-" + sortField.Value;
  767. }
  768. if (sortDescending)
  769. {
  770. filename += "-sortDescending";
  771. }
  772. filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture);
  773. return Path.Combine(
  774. _config.ApplicationPaths.CachePath,
  775. "channels",
  776. channelId,
  777. version,
  778. filename + ".json");
  779. }
  780. private static string GetIdToHash(string externalId, string channelName)
  781. {
  782. // Increment this as needed to force new downloads
  783. // Incorporate Name because it's being used to convert channel entity to provider
  784. return externalId + (channelName ?? string.Empty) + "16";
  785. }
  786. private T GetItemById<T>(string idString, string channelName, out bool isNew)
  787. where T : BaseItem, new()
  788. {
  789. var id = _libraryManager.GetNewItemId(GetIdToHash(idString, channelName), typeof(T));
  790. T item = null;
  791. try
  792. {
  793. item = _libraryManager.GetItemById(id) as T;
  794. }
  795. catch (Exception ex)
  796. {
  797. _logger.LogError(ex, "Error retrieving channel item from database");
  798. }
  799. if (item == null)
  800. {
  801. item = new T();
  802. isNew = true;
  803. }
  804. else
  805. {
  806. isNew = false;
  807. }
  808. item.Id = id;
  809. return item;
  810. }
  811. private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
  812. {
  813. var parentFolderId = parentFolder.Id;
  814. BaseItem item;
  815. bool isNew;
  816. bool forceUpdate = false;
  817. if (info.Type == ChannelItemType.Folder)
  818. {
  819. item = info.FolderType switch
  820. {
  821. ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew),
  822. ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew),
  823. ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew),
  824. ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew),
  825. ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew),
  826. _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew)
  827. };
  828. }
  829. else if (info.MediaType == ChannelMediaType.Audio)
  830. {
  831. item = info.ContentType == ChannelMediaContentType.Podcast
  832. ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew)
  833. : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew);
  834. }
  835. else
  836. {
  837. item = info.ContentType switch
  838. {
  839. ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew),
  840. ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew),
  841. var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer
  842. => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew),
  843. _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew)
  844. };
  845. }
  846. var enableMediaProbe = channelProvider is ISupportsMediaProbe;
  847. if (info.IsLiveStream)
  848. {
  849. item.RunTimeTicks = null;
  850. }
  851. else if (isNew || !enableMediaProbe)
  852. {
  853. item.RunTimeTicks = info.RunTimeTicks;
  854. }
  855. if (isNew)
  856. {
  857. item.Name = info.Name;
  858. item.Genres = info.Genres.ToArray();
  859. item.Studios = info.Studios.ToArray();
  860. item.CommunityRating = info.CommunityRating;
  861. item.Overview = info.Overview;
  862. item.IndexNumber = info.IndexNumber;
  863. item.ParentIndexNumber = info.ParentIndexNumber;
  864. item.PremiereDate = info.PremiereDate;
  865. item.ProductionYear = info.ProductionYear;
  866. item.ProviderIds = info.ProviderIds;
  867. item.OfficialRating = info.OfficialRating;
  868. item.DateCreated = info.DateCreated ?? DateTime.UtcNow;
  869. item.Tags = info.Tags.ToArray();
  870. item.OriginalTitle = info.OriginalTitle;
  871. }
  872. else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container)
  873. {
  874. // At least update names of container folders
  875. if (item.Name != info.Name)
  876. {
  877. item.Name = info.Name;
  878. forceUpdate = true;
  879. }
  880. }
  881. if (item is IHasArtist hasArtists)
  882. {
  883. hasArtists.Artists = info.Artists.ToArray();
  884. }
  885. if (item is IHasAlbumArtist hasAlbumArtists)
  886. {
  887. hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
  888. }
  889. if (item is Trailer trailer)
  890. {
  891. if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
  892. {
  893. _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
  894. forceUpdate = true;
  895. }
  896. trailer.TrailerTypes = info.TrailerTypes.ToArray();
  897. }
  898. if (info.DateModified > item.DateModified)
  899. {
  900. item.DateModified = info.DateModified;
  901. _logger.LogDebug("Forcing update due to DateModified {0}", item.Name);
  902. forceUpdate = true;
  903. }
  904. // was used for status
  905. // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
  906. //{
  907. // item.ExternalEtag = info.Etag;
  908. // forceUpdate = true;
  909. // _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
  910. //}
  911. if (!internalChannelId.Equals(item.ChannelId))
  912. {
  913. forceUpdate = true;
  914. _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
  915. }
  916. item.ChannelId = internalChannelId;
  917. if (!item.ParentId.Equals(parentFolderId))
  918. {
  919. forceUpdate = true;
  920. _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
  921. }
  922. item.ParentId = parentFolderId;
  923. if (item is IHasSeries hasSeries)
  924. {
  925. if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
  926. {
  927. forceUpdate = true;
  928. _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
  929. }
  930. hasSeries.SeriesName = info.SeriesName;
  931. }
  932. if (!string.Equals(item.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase))
  933. {
  934. forceUpdate = true;
  935. _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
  936. }
  937. item.ExternalId = info.Id;
  938. if (item is Audio channelAudioItem)
  939. {
  940. channelAudioItem.ExtraType = info.ExtraType;
  941. var mediaSource = info.MediaSources.FirstOrDefault();
  942. item.Path = mediaSource?.Path;
  943. }
  944. if (item is Video channelVideoItem)
  945. {
  946. channelVideoItem.ExtraType = info.ExtraType;
  947. var mediaSource = info.MediaSources.FirstOrDefault();
  948. item.Path = mediaSource?.Path;
  949. }
  950. if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
  951. {
  952. item.SetImagePath(ImageType.Primary, info.ImageUrl);
  953. _logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name);
  954. forceUpdate = true;
  955. }
  956. if (!info.IsLiveStream)
  957. {
  958. if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
  959. {
  960. item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
  961. _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
  962. forceUpdate = true;
  963. }
  964. }
  965. else
  966. {
  967. if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
  968. {
  969. item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
  970. _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
  971. forceUpdate = true;
  972. }
  973. }
  974. item.OnMetadataChanged();
  975. if (isNew)
  976. {
  977. _libraryManager.CreateItem(item, parentFolder);
  978. if (info.People != null && info.People.Count > 0)
  979. {
  980. _libraryManager.UpdatePeople(item, info.People);
  981. }
  982. }
  983. else if (forceUpdate)
  984. {
  985. item.UpdateToRepository(ItemUpdateType.None, cancellationToken);
  986. }
  987. if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
  988. {
  989. if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
  990. {
  991. SaveMediaSources(item, new List<MediaSourceInfo>());
  992. }
  993. else
  994. {
  995. SaveMediaSources(item, info.MediaSources);
  996. }
  997. }
  998. if (isNew || forceUpdate || item.DateLastRefreshed == default)
  999. {
  1000. _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
  1001. }
  1002. return item;
  1003. }
  1004. internal IChannel GetChannelProvider(Channel channel)
  1005. {
  1006. if (channel == null)
  1007. {
  1008. throw new ArgumentNullException(nameof(channel));
  1009. }
  1010. var result = GetAllChannels()
  1011. .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase));
  1012. if (result == null)
  1013. {
  1014. throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name);
  1015. }
  1016. return result;
  1017. }
  1018. internal IChannel GetChannelProvider(Guid internalChannelId)
  1019. {
  1020. var result = GetAllChannels()
  1021. .FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name)));
  1022. if (result == null)
  1023. {
  1024. throw new ResourceNotFoundException("No channel provider found for channel id " + internalChannelId);
  1025. }
  1026. return result;
  1027. }
  1028. }
  1029. }