< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Channels.ChannelManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
Line coverage
18%
Covered lines: 23
Uncovered lines: 103
Coverable lines: 126
Total lines: 1216
Line coverage: 18.2%
Branch coverage
9%
Covered branches: 4
Total branches: 41
Branch coverage: 9.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/Channels/ChannelManager.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.IO;
 7using System.Linq;
 8using System.Text.Json;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using AsyncKeyedLock;
 12using Jellyfin.Data.Entities;
 13using Jellyfin.Data.Enums;
 14using Jellyfin.Extensions;
 15using Jellyfin.Extensions.Json;
 16using MediaBrowser.Common.Extensions;
 17using MediaBrowser.Controller.Channels;
 18using MediaBrowser.Controller.Configuration;
 19using MediaBrowser.Controller.Dto;
 20using MediaBrowser.Controller.Entities;
 21using MediaBrowser.Controller.Entities.Audio;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.Providers;
 24using MediaBrowser.Model.Channels;
 25using MediaBrowser.Model.Dto;
 26using MediaBrowser.Model.Entities;
 27using MediaBrowser.Model.IO;
 28using MediaBrowser.Model.Querying;
 29using Microsoft.Extensions.Caching.Memory;
 30using Microsoft.Extensions.Logging;
 31using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 32using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 33using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 34using Season = MediaBrowser.Controller.Entities.TV.Season;
 35using Series = MediaBrowser.Controller.Entities.TV.Series;
 36
 37namespace Jellyfin.LiveTv.Channels
 38{
 39    /// <summary>
 40    /// The LiveTV channel manager.
 41    /// </summary>
 42    public class ChannelManager : IChannelManager, IDisposable
 43    {
 44        private readonly IUserManager _userManager;
 45        private readonly IUserDataManager _userDataManager;
 46        private readonly IDtoService _dtoService;
 47        private readonly ILibraryManager _libraryManager;
 48        private readonly ILogger<ChannelManager> _logger;
 49        private readonly IServerConfigurationManager _config;
 50        private readonly IFileSystem _fileSystem;
 51        private readonly IProviderManager _providerManager;
 52        private readonly IMemoryCache _memoryCache;
 2253        private readonly AsyncNonKeyedLocker _resourcePool = new(1);
 2254        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 55        private bool _disposed = false;
 56
 57        /// <summary>
 58        /// Initializes a new instance of the <see cref="ChannelManager"/> class.
 59        /// </summary>
 60        /// <param name="userManager">The user manager.</param>
 61        /// <param name="dtoService">The dto service.</param>
 62        /// <param name="libraryManager">The library manager.</param>
 63        /// <param name="logger">The logger.</param>
 64        /// <param name="config">The server configuration manager.</param>
 65        /// <param name="fileSystem">The filesystem.</param>
 66        /// <param name="userDataManager">The user data manager.</param>
 67        /// <param name="providerManager">The provider manager.</param>
 68        /// <param name="memoryCache">The memory cache.</param>
 69        /// <param name="channels">The channels.</param>
 70        public ChannelManager(
 71            IUserManager userManager,
 72            IDtoService dtoService,
 73            ILibraryManager libraryManager,
 74            ILogger<ChannelManager> logger,
 75            IServerConfigurationManager config,
 76            IFileSystem fileSystem,
 77            IUserDataManager userDataManager,
 78            IProviderManager providerManager,
 79            IMemoryCache memoryCache,
 80            IEnumerable<IChannel> channels)
 81        {
 2282            _userManager = userManager;
 2283            _dtoService = dtoService;
 2284            _libraryManager = libraryManager;
 2285            _logger = logger;
 2286            _config = config;
 2287            _fileSystem = fileSystem;
 2288            _userDataManager = userDataManager;
 2289            _providerManager = providerManager;
 2290            _memoryCache = memoryCache;
 2291            Channels = channels.ToArray();
 2292        }
 93
 94        internal IChannel[] Channels { get; }
 95
 096        private static TimeSpan CacheLength => TimeSpan.FromHours(3);
 97
 98        /// <inheritdoc />
 99        public bool EnableMediaSourceDisplay(BaseItem item)
 100        {
 0101            var internalChannel = _libraryManager.GetItemById(item.ChannelId);
 0102            var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 103
 0104            return channel is not IDisableMediaSourceDisplay;
 105        }
 106
 107        /// <inheritdoc />
 108        public bool CanDelete(BaseItem item)
 109        {
 0110            var internalChannel = _libraryManager.GetItemById(item.ChannelId);
 0111            var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 112
 0113            return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item);
 114        }
 115
 116        /// <inheritdoc />
 117        public Task DeleteItem(BaseItem item)
 118        {
 0119            var internalChannel = _libraryManager.GetItemById(item.ChannelId);
 0120            if (internalChannel is null)
 121            {
 0122                throw new ArgumentException(nameof(item.ChannelId));
 123            }
 124
 0125            var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 126
 0127            if (channel is not ISupportsDelete supportsDelete)
 128            {
 0129                throw new ArgumentException(nameof(channel));
 130            }
 131
 0132            return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
 133        }
 134
 135        private IEnumerable<IChannel> GetAllChannels()
 136        {
 1137            return Channels
 1138                .OrderBy(i => i.Name);
 139        }
 140
 141        /// <summary>
 142        /// Get the installed channel IDs.
 143        /// </summary>
 144        /// <returns>An <see cref="IEnumerable{T}"/> containing installed channel IDs.</returns>
 145        public IEnumerable<Guid> GetInstalledChannelIds()
 146        {
 0147            return GetAllChannels().Select(i => GetInternalChannelId(i.Name));
 148        }
 149
 150        /// <inheritdoc />
 151        public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
 152        {
 153            var user = query.UserId.IsEmpty()
 154                ? null
 155                : _userManager.GetUserById(query.UserId);
 156
 157            var channels = await GetAllChannelEntitiesAsync()
 158                .OrderBy(i => i.SortName)
 159                .ToListAsync()
 160                .ConfigureAwait(false);
 161
 162            if (query.IsRecordingsFolder.HasValue)
 163            {
 164                var val = query.IsRecordingsFolder.Value;
 165                channels = channels.Where(i =>
 166                {
 167                    try
 168                    {
 169                        return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
 170                            && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == v
 171                    }
 172                    catch
 173                    {
 174                        return false;
 175                    }
 176                }).ToList();
 177            }
 178
 179            if (query.SupportsLatestItems.HasValue)
 180            {
 181                var val = query.SupportsLatestItems.Value;
 182                channels = channels.Where(i =>
 183                {
 184                    try
 185                    {
 186                        return GetChannelProvider(i) is ISupportsLatestMedia == val;
 187                    }
 188                    catch
 189                    {
 190                        return false;
 191                    }
 192                }).ToList();
 193            }
 194
 195            if (query.SupportsMediaDeletion.HasValue)
 196            {
 197                var val = query.SupportsMediaDeletion.Value;
 198                channels = channels.Where(i =>
 199                {
 200                    try
 201                    {
 202                        return GetChannelProvider(i) is ISupportsDelete == val;
 203                    }
 204                    catch
 205                    {
 206                        return false;
 207                    }
 208                }).ToList();
 209            }
 210
 211            if (query.IsFavorite.HasValue)
 212            {
 213                var val = query.IsFavorite.Value;
 214                channels = channels.Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val)
 215                    .ToList();
 216            }
 217
 218            if (user is not null)
 219            {
 220                var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
 221                channels = channels.Where(i =>
 222                {
 223                    if (!i.IsVisible(user))
 224                    {
 225                        return false;
 226                    }
 227
 228                    try
 229                    {
 230                        return GetChannelProvider(i).IsEnabledFor(userId);
 231                    }
 232                    catch
 233                    {
 234                        return false;
 235                    }
 236                }).ToList();
 237            }
 238
 239            var all = channels;
 240            var totalCount = all.Count;
 241
 242            if (query.StartIndex.HasValue || query.Limit.HasValue)
 243            {
 244                int startIndex = query.StartIndex ?? 0;
 245                int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - sta
 246                all = all.GetRange(startIndex, count);
 247            }
 248
 249            if (query.RefreshLatestChannelItems)
 250            {
 251                foreach (var item in all)
 252                {
 253                    await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(fal
 254                }
 255            }
 256
 257            return new QueryResult<Channel>(
 258                query.StartIndex,
 259                totalCount,
 260                all);
 261        }
 262
 263        /// <inheritdoc />
 264        public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
 265        {
 266            var user = query.UserId.IsEmpty()
 267                ? null
 268                : _userManager.GetUserById(query.UserId);
 269
 270            var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
 271
 272            var dtoOptions = new DtoOptions();
 273
 274            // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can gener
 275            var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
 276
 277            var result = new QueryResult<BaseItemDto>(
 278                query.StartIndex,
 279                internalResult.TotalRecordCount,
 280                returnItems);
 281
 282            return result;
 283        }
 284
 285        /// <summary>
 286        /// Refreshes the associated channels.
 287        /// </summary>
 288        /// <param name="progress">The progress.</param>
 289        /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
 290        /// <returns>The completed task.</returns>
 291        public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken)
 292        {
 293            var allChannelsList = GetAllChannels().ToList();
 294
 295            var numComplete = 0;
 296
 297            foreach (var channelInfo in allChannelsList)
 298            {
 299                cancellationToken.ThrowIfCancellationRequested();
 300
 301                try
 302                {
 303                    await GetChannel(channelInfo, cancellationToken).ConfigureAwait(false);
 304                }
 305                catch (OperationCanceledException)
 306                {
 307                    throw;
 308                }
 309                catch (Exception ex)
 310                {
 311                    _logger.LogError(ex, "Error getting channel information for {0}", channelInfo.Name);
 312                }
 313
 314                numComplete++;
 315                double percent = (double)numComplete / allChannelsList.Count;
 316                progress.Report(100 * percent);
 317            }
 318
 319            progress.Report(100);
 320        }
 321
 322        private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
 323        {
 324            foreach (IChannel channel in GetAllChannels())
 325            {
 326                yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationTok
 327            }
 328        }
 329
 330        private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
 331        {
 0332            var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
 333
 334            try
 335            {
 0336                var bytes = File.ReadAllBytes(path);
 0337                return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
 0338                    ?? Array.Empty<MediaSourceInfo>();
 339            }
 0340            catch
 341            {
 0342                return Array.Empty<MediaSourceInfo>();
 343            }
 0344        }
 345
 346        private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
 347        {
 348            var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
 349
 350            if (mediaSources is null || mediaSources.Count == 0)
 351            {
 352                try
 353                {
 354                    _fileSystem.DeleteFile(path);
 355                }
 356                catch
 357                {
 358                }
 359
 360                return;
 361            }
 362
 363            Directory.CreateDirectory(Path.GetDirectoryName(path));
 364
 365            FileStream createStream = File.Create(path);
 366            await using (createStream.ConfigureAwait(false))
 367            {
 368                await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
 369            }
 370        }
 371
 372        /// <inheritdoc />
 373        public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken)
 374        {
 0375            IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item);
 376
 0377            return results
 0378                .Select(i => NormalizeMediaSource(item, i))
 0379                .ToList();
 380        }
 381
 382        /// <summary>
 383        /// Gets the dynamic media sources based on the provided item.
 384        /// </summary>
 385        /// <param name="item">The item.</param>
 386        /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
 387        /// <returns>The task representing the operation to get the media sources.</returns>
 388        public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancella
 389        {
 390            var channel = GetChannel(item.ChannelId);
 391            var channelPlugin = GetChannelProvider(channel);
 392
 393            IEnumerable<MediaSourceInfo> results;
 394
 395            if (channelPlugin is IRequiresMediaInfoCallback requiresCallback)
 396            {
 397                results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
 398                    .ConfigureAwait(false);
 399            }
 400            else
 401            {
 402                results = Enumerable.Empty<MediaSourceInfo>();
 403            }
 404
 405            return results
 406                .Select(i => NormalizeMediaSource(item, i))
 407                .ToList();
 408        }
 409
 410        private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback c
 411        {
 412            if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
 413            {
 414                return cachedInfo;
 415            }
 416
 417            var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
 418                   .ConfigureAwait(false);
 419            var list = mediaInfo.ToList();
 420            _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
 421
 422            return list;
 423        }
 424
 425        private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info)
 426        {
 0427            info.RunTimeTicks ??= item.RunTimeTicks;
 428
 0429            return info;
 430        }
 431
 432        private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken)
 433        {
 434            var parentFolderId = Guid.Empty;
 435
 436            var id = GetInternalChannelId(channelInfo.Name);
 437
 438            var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id);
 439
 440            var isNew = false;
 441            var forceUpdate = false;
 442
 443            var item = _libraryManager.GetItemById(id) as Channel;
 444
 445            if (item is null)
 446            {
 447                item = new Channel
 448                {
 449                    Name = channelInfo.Name,
 450                    Id = id,
 451                    DateCreated = _fileSystem.GetCreationTimeUtc(path),
 452                    DateModified = _fileSystem.GetLastWriteTimeUtc(path)
 453                };
 454
 455                isNew = true;
 456            }
 457
 458            if (!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 459            {
 460                isNew = true;
 461            }
 462
 463            item.Path = path;
 464
 465            if (!item.ChannelId.Equals(id))
 466            {
 467                forceUpdate = true;
 468            }
 469
 470            item.ChannelId = id;
 471
 472            if (!item.ParentId.Equals(parentFolderId))
 473            {
 474                forceUpdate = true;
 475            }
 476
 477            item.ParentId = parentFolderId;
 478
 479            item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
 480            item.Overview = channelInfo.Description;
 481
 482            if (string.IsNullOrWhiteSpace(item.Name))
 483            {
 484                item.Name = channelInfo.Name;
 485            }
 486
 487            if (isNew)
 488            {
 489                item.OnMetadataChanged();
 490                _libraryManager.CreateItem(item, null);
 491            }
 492
 493            await item.RefreshMetadata(
 494                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 495                {
 496                    ForceSave = !isNew && forceUpdate
 497                },
 498                cancellationToken).ConfigureAwait(false);
 499
 500            return item;
 501        }
 502
 503        private static string GetOfficialRating(ChannelParentalRating rating)
 504        {
 0505            return rating switch
 0506            {
 0507                ChannelParentalRating.Adult => "XXX",
 0508                ChannelParentalRating.UsR => "R",
 0509                ChannelParentalRating.UsPG13 => "PG-13",
 0510                ChannelParentalRating.UsPG => "PG",
 0511                _ => null
 0512            };
 513        }
 514
 515        /// <summary>
 516        /// Gets a channel with the provided Guid.
 517        /// </summary>
 518        /// <param name="id">The Guid.</param>
 519        /// <returns>The corresponding channel.</returns>
 520        public Channel GetChannel(Guid id)
 521        {
 0522            return _libraryManager.GetItemById(id) as Channel;
 523        }
 524
 525        /// <inheritdoc />
 526        public Channel GetChannel(string id)
 527        {
 0528            return _libraryManager.GetItemById(id) as Channel;
 529        }
 530
 531        /// <inheritdoc />
 532        public ChannelFeatures[] GetAllChannelFeatures()
 533        {
 0534            return _libraryManager.GetItemIds(
 0535                new InternalItemsQuery
 0536                {
 0537                    IncludeItemTypes = new[] { BaseItemKind.Channel },
 0538                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 0539                }).Select(i => GetChannelFeatures(i)).ToArray();
 540        }
 541
 542        /// <inheritdoc />
 543        public ChannelFeatures GetChannelFeatures(Guid? id)
 544        {
 0545            if (!id.HasValue)
 546            {
 0547                throw new ArgumentNullException(nameof(id));
 548            }
 549
 0550            var channel = GetChannel(id.Value);
 0551            var channelProvider = GetChannelProvider(channel);
 552
 0553            return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures());
 554        }
 555
 556        /// <summary>
 557        /// Gets the provided channel's supported features.
 558        /// </summary>
 559        /// <param name="channel">The channel.</param>
 560        /// <param name="provider">The provider.</param>
 561        /// <param name="features">The features.</param>
 562        /// <returns>The supported features.</returns>
 563        public ChannelFeatures GetChannelFeaturesDto(
 564            Channel channel,
 565            IChannel provider,
 566            InternalChannelFeatures features)
 567        {
 0568            var supportsLatest = provider is ISupportsLatestMedia;
 569
 0570            return new ChannelFeatures(channel.Name, channel.Id)
 0571            {
 0572                CanFilter = !features.MaxPageSize.HasValue,
 0573                ContentTypes = features.ContentTypes.ToArray(),
 0574                DefaultSortFields = features.DefaultSortFields.ToArray(),
 0575                MaxPageSize = features.MaxPageSize,
 0576                MediaTypes = features.MediaTypes.ToArray(),
 0577                SupportsSortOrderToggle = features.SupportsSortOrderToggle,
 0578                SupportsLatestMedia = supportsLatest,
 0579                SupportsContentDownloading = features.SupportsContentDownloading,
 0580                AutoRefreshLevels = features.AutoRefreshLevels
 0581            };
 582        }
 583
 584        private Guid GetInternalChannelId(string name)
 585        {
 0586            ArgumentException.ThrowIfNullOrEmpty(name);
 587
 0588            return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
 589        }
 590
 591        /// <inheritdoc />
 592        public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken ca
 593        {
 594            var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false);
 595
 596            var items = internalResult.Items;
 597            var totalRecordCount = internalResult.TotalRecordCount;
 598
 599            var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User);
 600
 601            var result = new QueryResult<BaseItemDto>(
 602                query.StartIndex,
 603                totalRecordCount,
 604                returnItems);
 605
 606            return result;
 607        }
 608
 609        /// <inheritdoc />
 610        public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationTok
 611        {
 612            var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
 613
 614            if (query.ChannelIds.Count > 0)
 615            {
 616                // Avoid implicitly captured closure
 617                var ids = query.ChannelIds;
 618                channels = channels
 619                    .Where(i => ids.Contains(GetInternalChannelId(i.Name)))
 620                    .ToArray();
 621            }
 622
 623            if (channels.Length == 0)
 624            {
 625                return new QueryResult<BaseItem>();
 626            }
 627
 628            foreach (var channel in channels)
 629            {
 630                await RefreshLatestChannelItems(channel, cancellationToken).ConfigureAwait(false);
 631            }
 632
 633            query.IsFolder = false;
 634
 635            // hack for trailers, figure out a better way later
 636            var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringCompar
 637
 638            if (sortByPremiereDate)
 639            {
 640                query.OrderBy = new[]
 641                {
 642                    (ItemSortBy.PremiereDate, SortOrder.Descending),
 643                    (ItemSortBy.ProductionYear, SortOrder.Descending),
 644                    (ItemSortBy.DateCreated, SortOrder.Descending)
 645                };
 646            }
 647            else
 648            {
 649                query.OrderBy = new[]
 650                {
 651                    (ItemSortBy.DateCreated, SortOrder.Descending)
 652                };
 653            }
 654
 655            return _libraryManager.GetItemsResult(query);
 656        }
 657
 658        private async Task RefreshLatestChannelItems(IChannel channel, CancellationToken cancellationToken)
 659        {
 660            var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
 661
 662            var query = new InternalItemsQuery
 663            {
 664                Parent = internalChannel,
 665                EnableTotalRecordCount = false,
 666                ChannelIds = new Guid[] { internalChannel.Id }
 667            };
 668
 669            var result = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).ConfigureAwait(
 670
 671            foreach (var item in result.Items)
 672            {
 673                if (item is Folder folder)
 674                {
 675                    await GetChannelItemsInternal(
 676                        new InternalItemsQuery
 677                        {
 678                            Parent = folder,
 679                            EnableTotalRecordCount = false,
 680                            ChannelIds = new Guid[] { internalChannel.Id }
 681                        },
 682                        new Progress<double>(),
 683                        cancellationToken).ConfigureAwait(false);
 684                }
 685            }
 686        }
 687
 688        /// <inheritdoc />
 689        public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> pro
 690        {
 691            // Get the internal channel entity
 692            var channel = GetChannel(query.ChannelIds[0]);
 693
 694            // Find the corresponding channel provider plugin
 695            var channelProvider = GetChannelProvider(channel);
 696
 697            var parentItem = query.ParentId.IsEmpty()
 698                ? channel
 699                : _libraryManager.GetItemById(query.ParentId);
 700
 701            var itemsResult = await GetChannelItems(
 702                channelProvider,
 703                query.User,
 704                parentItem is Channel ? null : parentItem.ExternalId,
 705                null,
 706                false,
 707                cancellationToken)
 708                .ConfigureAwait(false);
 709
 710            if (query.ParentId.IsEmpty())
 711            {
 712                query.Parent = channel;
 713            }
 714
 715            query.ChannelIds = Array.Empty<Guid>();
 716
 717            // Not yet sure why this is causing a problem
 718            query.GroupByPresentationUniqueKey = false;
 719
 720            // null if came from cache
 721            if (itemsResult is not null)
 722            {
 723                var items = itemsResult.Items;
 724                var itemsLen = items.Count;
 725                var internalItems = new Guid[itemsLen];
 726                for (int i = 0; i < itemsLen; i++)
 727                {
 728                    internalItems[i] = (await GetChannelItemEntityAsync(
 729                        items[i],
 730                        channelProvider,
 731                        channel.Id,
 732                        parentItem,
 733                        cancellationToken).ConfigureAwait(false)).Id;
 734                }
 735
 736                var existingIds = _libraryManager.GetItemIds(query);
 737                var deadIds = existingIds.Except(internalItems)
 738                    .ToArray();
 739
 740                foreach (var deadId in deadIds)
 741                {
 742                    var deadItem = _libraryManager.GetItemById(deadId);
 743                    if (deadItem is not null)
 744                    {
 745                        _libraryManager.DeleteItem(
 746                            deadItem,
 747                            new DeleteOptions
 748                            {
 749                                DeleteFileLocation = false,
 750                                DeleteFromExternalProvider = false
 751                            },
 752                            parentItem,
 753                            false);
 754                    }
 755                }
 756            }
 757
 758            return _libraryManager.GetItemsResult(query);
 759        }
 760
 761        /// <inheritdoc />
 762        public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancella
 763        {
 764            var internalResult = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).Configu
 765
 766            var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User);
 767
 768            var result = new QueryResult<BaseItemDto>(
 769                query.StartIndex,
 770                internalResult.TotalRecordCount,
 771                returnItems);
 772
 773            return result;
 774        }
 775
 776        private async Task<ChannelItemResult> GetChannelItems(
 777            IChannel channel,
 778            User user,
 779            string externalFolderId,
 780            ChannelItemSortField? sortField,
 781            bool sortDescending,
 782            CancellationToken cancellationToken)
 783        {
 784            var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
 785
 786            var cacheLength = CacheLength;
 787            var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
 788
 789            try
 790            {
 791                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
 792                {
 793                    var jsonStream = AsyncFile.OpenRead(cachePath);
 794                    await using (jsonStream.ConfigureAwait(false))
 795                    {
 796                        var cachedResult = await JsonSerializer
 797                            .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
 798                            .ConfigureAwait(false);
 799                        if (cachedResult is not null)
 800                        {
 801                            return null;
 802                        }
 803                    }
 804                }
 805            }
 806            catch (FileNotFoundException)
 807            {
 808            }
 809            catch (IOException)
 810            {
 811            }
 812
 813            using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 814            {
 815                try
 816                {
 817                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
 818                    {
 819                        var jsonStream = AsyncFile.OpenRead(cachePath);
 820                        await using (jsonStream.ConfigureAwait(false))
 821                        {
 822                            var cachedResult = await JsonSerializer
 823                                .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
 824                                .ConfigureAwait(false);
 825                            if (cachedResult is not null)
 826                            {
 827                                return null;
 828                            }
 829                        }
 830                    }
 831                }
 832                catch (FileNotFoundException)
 833                {
 834                }
 835                catch (IOException)
 836                {
 837                }
 838
 839                var query = new InternalChannelItemQuery
 840                {
 841                    UserId = user?.Id ?? Guid.Empty,
 842                    SortBy = sortField,
 843                    SortDescending = sortDescending,
 844                    FolderId = externalFolderId
 845                };
 846
 847                query.FolderId = externalFolderId;
 848
 849                var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false);
 850
 851                if (result is null)
 852                {
 853                    throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
 854                }
 855
 856                await CacheResponse(result, cachePath).ConfigureAwait(false);
 857
 858                return result;
 859            }
 860        }
 861
 862        private async Task CacheResponse(ChannelItemResult result, string path)
 863        {
 864            try
 865            {
 866                Directory.CreateDirectory(Path.GetDirectoryName(path));
 867
 868                var createStream = File.Create(path);
 869                await using (createStream.ConfigureAwait(false))
 870                {
 871                    await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
 872                }
 873            }
 874            catch (Exception ex)
 875            {
 876                _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
 877            }
 878        }
 879
 880        private string GetChannelDataCachePath(
 881            IChannel channel,
 882            string userId,
 883            string externalFolderId,
 884            ChannelItemSortField? sortField,
 885            bool sortDescending)
 886        {
 0887            var channelId = GetInternalChannelId(channel.Name).ToString("N", CultureInfo.InvariantCulture);
 888
 0889            var userCacheKey = string.Empty;
 890
 0891            if (channel is IHasCacheKey hasCacheKey)
 892            {
 0893                userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty;
 894            }
 895
 0896            var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N", Cul
 0897            filename += userCacheKey;
 898
 0899            var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N", CultureInfo.InvariantCult
 900
 0901            if (sortField.HasValue)
 902            {
 0903                filename += "-sortField-" + sortField.Value;
 904            }
 905
 0906            if (sortDescending)
 907            {
 0908                filename += "-sortDescending";
 909            }
 910
 0911            filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 912
 0913            return Path.Combine(
 0914                _config.ApplicationPaths.CachePath,
 0915                "channels",
 0916                channelId,
 0917                version,
 0918                filename + ".json");
 919        }
 920
 921        private static string GetIdToHash(string externalId, string channelName)
 922        {
 923            // Increment this as needed to force new downloads
 924            // Incorporate Name because it's being used to convert channel entity to provider
 0925            return externalId + (channelName ?? string.Empty) + "16";
 926        }
 927
 928        private T GetItemById<T>(string idString, string channelName, out bool isNew)
 929            where T : BaseItem, new()
 930        {
 0931            var id = _libraryManager.GetNewItemId(GetIdToHash(idString, channelName), typeof(T));
 932
 0933            T item = null;
 934
 935            try
 936            {
 0937                item = _libraryManager.GetItemById(id) as T;
 0938            }
 0939            catch (Exception ex)
 940            {
 0941                _logger.LogError(ex, "Error retrieving channel item from database");
 0942            }
 943
 0944            if (item is null)
 945            {
 0946                item = new T();
 0947                isNew = true;
 948            }
 949            else
 950            {
 0951                isNew = false;
 952            }
 953
 0954            item.Id = id;
 0955            return item;
 956        }
 957
 958        private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid inte
 959        {
 960            var parentFolderId = parentFolder.Id;
 961
 962            BaseItem item;
 963            bool isNew;
 964            bool forceUpdate = false;
 965
 966            if (info.Type == ChannelItemType.Folder)
 967            {
 968                item = info.FolderType switch
 969                {
 970                    ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew),
 971                    ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew),
 972                    ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew),
 973                    ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew),
 974                    ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew),
 975                    _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew)
 976                };
 977            }
 978            else if (info.MediaType == ChannelMediaType.Audio)
 979            {
 980                item = info.ContentType == ChannelMediaContentType.Podcast
 981                    ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew)
 982                    : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew);
 983            }
 984            else
 985            {
 986                item = info.ContentType switch
 987                {
 988                    ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew),
 989                    ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew),
 990                    var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer
 991                    => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew),
 992                    _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew)
 993                };
 994            }
 995
 996            var enableMediaProbe = channelProvider is ISupportsMediaProbe;
 997
 998            if (info.IsLiveStream)
 999            {
 1000                item.RunTimeTicks = null;
 1001            }
 1002            else if (isNew || !enableMediaProbe)
 1003            {
 1004                item.RunTimeTicks = info.RunTimeTicks;
 1005            }
 1006
 1007            if (isNew)
 1008            {
 1009                item.Name = info.Name;
 1010                item.Genres = info.Genres.ToArray();
 1011                item.Studios = info.Studios.ToArray();
 1012                item.CommunityRating = info.CommunityRating;
 1013                item.Overview = info.Overview;
 1014                item.IndexNumber = info.IndexNumber;
 1015                item.ParentIndexNumber = info.ParentIndexNumber;
 1016                item.PremiereDate = info.PremiereDate;
 1017                item.ProductionYear = info.ProductionYear;
 1018                item.ProviderIds = info.ProviderIds;
 1019                item.OfficialRating = info.OfficialRating;
 1020                item.DateCreated = info.DateCreated ?? DateTime.UtcNow;
 1021                item.Tags = info.Tags.ToArray();
 1022                item.OriginalTitle = info.OriginalTitle;
 1023            }
 1024            else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container)
 1025            {
 1026                // At least update names of container folders
 1027                if (item.Name != info.Name)
 1028                {
 1029                    item.Name = info.Name;
 1030                    forceUpdate = true;
 1031                }
 1032            }
 1033
 1034            if (item is IHasArtist hasArtists)
 1035            {
 1036                hasArtists.Artists = info.Artists.ToArray();
 1037            }
 1038
 1039            if (item is IHasAlbumArtist hasAlbumArtists)
 1040            {
 1041                hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
 1042            }
 1043
 1044            if (item is Trailer trailer)
 1045            {
 1046                if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
 1047                {
 1048                    _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
 1049                    forceUpdate = true;
 1050                }
 1051
 1052                trailer.TrailerTypes = info.TrailerTypes.ToArray();
 1053            }
 1054
 1055            if (info.DateModified > item.DateModified)
 1056            {
 1057                item.DateModified = info.DateModified;
 1058                _logger.LogDebug("Forcing update due to DateModified {0}", item.Name);
 1059                forceUpdate = true;
 1060            }
 1061
 1062            if (!internalChannelId.Equals(item.ChannelId))
 1063            {
 1064                forceUpdate = true;
 1065                _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
 1066            }
 1067
 1068            item.ChannelId = internalChannelId;
 1069
 1070            if (!item.ParentId.Equals(parentFolderId))
 1071            {
 1072                forceUpdate = true;
 1073                _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
 1074            }
 1075
 1076            item.ParentId = parentFolderId;
 1077
 1078            if (item is IHasSeries hasSeries)
 1079            {
 1080                if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
 1081                {
 1082                    forceUpdate = true;
 1083                    _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
 1084                }
 1085
 1086                hasSeries.SeriesName = info.SeriesName;
 1087            }
 1088
 1089            if (!string.Equals(item.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase))
 1090            {
 1091                forceUpdate = true;
 1092                _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
 1093            }
 1094
 1095            item.ExternalId = info.Id;
 1096
 1097            if (item is Audio channelAudioItem)
 1098            {
 1099                channelAudioItem.ExtraType = info.ExtraType;
 1100
 1101                var mediaSource = info.MediaSources.FirstOrDefault();
 1102                item.Path = mediaSource?.Path;
 1103            }
 1104
 1105            if (item is Video channelVideoItem)
 1106            {
 1107                channelVideoItem.ExtraType = info.ExtraType;
 1108
 1109                var mediaSource = info.MediaSources.FirstOrDefault();
 1110                item.Path = mediaSource?.Path;
 1111            }
 1112
 1113            if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
 1114            {
 1115                item.SetImagePath(ImageType.Primary, info.ImageUrl);
 1116                _logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name);
 1117                forceUpdate = true;
 1118            }
 1119
 1120            if (!info.IsLiveStream)
 1121            {
 1122                if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
 1123                {
 1124                    item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
 1125                    _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
 1126                    forceUpdate = true;
 1127                }
 1128            }
 1129            else
 1130            {
 1131                if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
 1132                {
 1133                    item.Tags = [..item.Tags, "livestream"];
 1134                    _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
 1135                    forceUpdate = true;
 1136                }
 1137            }
 1138
 1139            item.OnMetadataChanged();
 1140
 1141            if (isNew)
 1142            {
 1143                _libraryManager.CreateItem(item, parentFolder);
 1144
 1145                if (info.People is not null && info.People.Count > 0)
 1146                {
 1147                    await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
 1148                }
 1149            }
 1150            else if (forceUpdate)
 1151            {
 1152                await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
 1153            }
 1154
 1155            if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
 1156            {
 1157                if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
 1158                {
 1159                    await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
 1160                }
 1161                else
 1162                {
 1163                    await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
 1164                }
 1165            }
 1166
 1167            if (isNew || forceUpdate || item.DateLastRefreshed == default)
 1168            {
 1169                _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Re
 1170            }
 1171
 1172            return item;
 1173        }
 1174
 1175        internal IChannel GetChannelProvider(Channel channel)
 1176        {
 01177            ArgumentNullException.ThrowIfNull(channel);
 1178
 01179            var result = GetAllChannels()
 01180                .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, cha
 1181
 01182            if (result is null)
 1183            {
 01184                throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name);
 1185            }
 1186
 01187            return result;
 1188        }
 1189
 1190        /// <inheritdoc />
 1191        public void Dispose()
 1192        {
 221193            Dispose(true);
 221194            GC.SuppressFinalize(this);
 221195        }
 1196
 1197        /// <summary>
 1198        /// Releases unmanaged and optionally managed resources.
 1199        /// </summary>
 1200        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 1201        protected virtual void Dispose(bool disposing)
 1202        {
 221203            if (_disposed)
 1204            {
 01205                return;
 1206            }
 1207
 221208            if (disposing)
 1209            {
 221210                _resourcePool?.Dispose();
 1211            }
 1212
 221213            _disposed = true;
 221214        }
 1215    }
 1216}

Methods/Properties

.ctor(MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Dto.IDtoService,MediaBrowser.Controller.Library.ILibraryManager,Microsoft.Extensions.Logging.ILogger`1<Jellyfin.LiveTv.Channels.ChannelManager>,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.Library.IUserDataManager,MediaBrowser.Controller.Providers.IProviderManager,Microsoft.Extensions.Caching.Memory.IMemoryCache,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Channels.IChannel>)
get_CacheLength()
EnableMediaSourceDisplay(MediaBrowser.Controller.Entities.BaseItem)
CanDelete(MediaBrowser.Controller.Entities.BaseItem)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem)
GetAllChannels()
GetInstalledChannelIds()
GetSavedMediaSources(MediaBrowser.Controller.Entities.BaseItem)
GetStaticMediaSources(MediaBrowser.Controller.Entities.BaseItem,System.Threading.CancellationToken)
NormalizeMediaSource(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceInfo)
GetOfficialRating(MediaBrowser.Controller.Channels.ChannelParentalRating)
GetChannel(System.Guid)
GetChannel(System.String)
GetAllChannelFeatures()
GetChannelFeatures(System.Nullable`1<System.Guid>)
GetChannelFeaturesDto(MediaBrowser.Controller.Channels.Channel,MediaBrowser.Controller.Channels.IChannel,MediaBrowser.Controller.Channels.InternalChannelFeatures)
GetInternalChannelId(System.String)
GetChannelDataCachePath(MediaBrowser.Controller.Channels.IChannel,System.String,System.String,System.Nullable`1<MediaBrowser.Model.Channels.ChannelItemSortField>,System.Boolean)
GetIdToHash(System.String,System.String)
GetItemById(System.String,System.String,System.Boolean&)
GetChannelProvider(MediaBrowser.Controller.Channels.Channel)
Dispose()
Dispose(System.Boolean)