< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Channels.ChannelManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
Line coverage
11%
Covered lines: 65
Uncovered lines: 520
Coverable lines: 585
Total lines: 1215
Line coverage: 11.1%
Branch coverage
5%
Covered branches: 13
Total branches: 223
Branch coverage: 5.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 AM Line coverage: 18.2% (23/126) Branch coverage: 9.7% (4/41) Total lines: 12154/19/2026 - 12:14:27 AM Line coverage: 11% (65/586) Branch coverage: 6.2% (14/225) Total lines: 12155/20/2026 - 12:15:44 AM Line coverage: 11% (65/586) Branch coverage: 5.7% (13/225) Total lines: 12156/28/2026 - 12:15:35 AM Line coverage: 11.1% (65/585) Branch coverage: 5.8% (13/223) Total lines: 1215 3/26/2026 - 12:14:14 AM Line coverage: 18.2% (23/126) Branch coverage: 9.7% (4/41) Total lines: 12154/19/2026 - 12:14:27 AM Line coverage: 11% (65/586) Branch coverage: 6.2% (14/225) Total lines: 12155/20/2026 - 12:15:44 AM Line coverage: 11% (65/586) Branch coverage: 5.7% (13/225) Total lines: 12156/28/2026 - 12:15:35 AM Line coverage: 11.1% (65/585) Branch coverage: 5.8% (13/223) Total lines: 1215

Coverage delta

Coverage delta 8 -8

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.Enums;
 13using Jellyfin.Database.Implementations.Entities;
 14using Jellyfin.Database.Implementations.Enums;
 15using Jellyfin.Extensions;
 16using Jellyfin.Extensions.Json;
 17using Jellyfin.LiveTv;
 18using MediaBrowser.Common.Extensions;
 19using MediaBrowser.Controller.Channels;
 20using MediaBrowser.Controller.Configuration;
 21using MediaBrowser.Controller.Dto;
 22using MediaBrowser.Controller.Entities;
 23using MediaBrowser.Controller.Entities.Audio;
 24using MediaBrowser.Controller.Library;
 25using MediaBrowser.Controller.Providers;
 26using MediaBrowser.Model.Channels;
 27using MediaBrowser.Model.Dto;
 28using MediaBrowser.Model.Entities;
 29using MediaBrowser.Model.IO;
 30using MediaBrowser.Model.Querying;
 31using Microsoft.Extensions.Caching.Memory;
 32using Microsoft.Extensions.Logging;
 33using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 34using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 35using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 36using Season = MediaBrowser.Controller.Entities.TV.Season;
 37using Series = MediaBrowser.Controller.Entities.TV.Series;
 38
 39namespace Jellyfin.LiveTv.Channels
 40{
 41    /// <summary>
 42    /// The LiveTV channel manager.
 43    /// </summary>
 44    public class ChannelManager : IChannelManager, IDisposable
 45    {
 46        private readonly IUserManager _userManager;
 47        private readonly IUserDataManager _userDataManager;
 48        private readonly IDtoService _dtoService;
 49        private readonly ILibraryManager _libraryManager;
 50        private readonly ILogger<ChannelManager> _logger;
 51        private readonly IServerConfigurationManager _config;
 52        private readonly IFileSystem _fileSystem;
 53        private readonly IProviderManager _providerManager;
 54        private readonly IMemoryCache _memoryCache;
 2255        private readonly AsyncNonKeyedLocker _resourcePool = new(1);
 2256        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 57        private bool _disposed = false;
 58
 59        /// <summary>
 60        /// Initializes a new instance of the <see cref="ChannelManager"/> class.
 61        /// </summary>
 62        /// <param name="userManager">The user manager.</param>
 63        /// <param name="dtoService">The dto service.</param>
 64        /// <param name="libraryManager">The library manager.</param>
 65        /// <param name="logger">The logger.</param>
 66        /// <param name="config">The server configuration manager.</param>
 67        /// <param name="fileSystem">The filesystem.</param>
 68        /// <param name="userDataManager">The user data manager.</param>
 69        /// <param name="providerManager">The provider manager.</param>
 70        /// <param name="memoryCache">The memory cache.</param>
 71        /// <param name="channels">The channels.</param>
 72        public ChannelManager(
 73            IUserManager userManager,
 74            IDtoService dtoService,
 75            ILibraryManager libraryManager,
 76            ILogger<ChannelManager> logger,
 77            IServerConfigurationManager config,
 78            IFileSystem fileSystem,
 79            IUserDataManager userDataManager,
 80            IProviderManager providerManager,
 81            IMemoryCache memoryCache,
 82            IEnumerable<IChannel> channels)
 83        {
 2284            _userManager = userManager;
 2285            _dtoService = dtoService;
 2286            _libraryManager = libraryManager;
 2287            _logger = logger;
 2288            _config = config;
 2289            _fileSystem = fileSystem;
 2290            _userDataManager = userDataManager;
 2291            _providerManager = providerManager;
 2292            _memoryCache = memoryCache;
 2293            Channels = channels.ToArray();
 2294        }
 95
 96        internal IChannel[] Channels { get; }
 97
 098        private static TimeSpan CacheLength => TimeSpan.FromHours(3);
 99
 100        /// <inheritdoc />
 101        public bool EnableMediaSourceDisplay(BaseItem item)
 102        {
 0103            var internalChannel = _libraryManager.GetItemById(item.ChannelId);
 0104            var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 105
 0106            return channel is not IDisableMediaSourceDisplay;
 107        }
 108
 109        /// <inheritdoc />
 110        public bool CanDelete(BaseItem item)
 111        {
 0112            var internalChannel = _libraryManager.GetItemById(item.ChannelId);
 0113            var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 114
 0115            return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item);
 116        }
 117
 118        /// <inheritdoc />
 119        public Task DeleteItem(BaseItem item)
 120        {
 0121            var internalChannel = _libraryManager.GetItemById(item.ChannelId);
 0122            if (internalChannel is null)
 123            {
 0124                throw new ArgumentException(nameof(item.ChannelId));
 125            }
 126
 0127            var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 128
 0129            if (channel is not ISupportsDelete supportsDelete)
 130            {
 0131                throw new ArgumentException(nameof(channel));
 132            }
 133
 0134            return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
 135        }
 136
 137        private IEnumerable<IChannel> GetAllChannels()
 138        {
 1139            return Channels
 1140                .OrderBy(i => i.Name);
 141        }
 142
 143        /// <summary>
 144        /// Get the installed channel IDs.
 145        /// </summary>
 146        /// <returns>An <see cref="IEnumerable{T}"/> containing installed channel IDs.</returns>
 147        public IEnumerable<Guid> GetInstalledChannelIds()
 148        {
 0149            return GetAllChannels().Select(i => GetInternalChannelId(i.Name));
 150        }
 151
 152        /// <inheritdoc />
 153        public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
 154        {
 1155            var user = query.UserId.IsEmpty()
 1156                ? null
 1157                : _userManager.GetUserById(query.UserId);
 158
 1159            var channels = await GetAllChannelEntitiesAsync()
 1160                .OrderBy(i => i.SortName)
 1161                .ToListAsync()
 1162                .ConfigureAwait(false);
 163
 1164            if (query.IsRecordingsFolder.HasValue)
 165            {
 0166                var val = query.IsRecordingsFolder.Value;
 0167                channels = channels.Where(i =>
 0168                {
 0169                    try
 0170                    {
 0171                        return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
 0172                            && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == v
 0173                    }
 0174                    catch
 0175                    {
 0176                        return false;
 0177                    }
 0178                }).ToList();
 179            }
 180
 1181            if (query.SupportsLatestItems.HasValue)
 182            {
 0183                var val = query.SupportsLatestItems.Value;
 0184                channels = channels.Where(i =>
 0185                {
 0186                    try
 0187                    {
 0188                        return GetChannelProvider(i) is ISupportsLatestMedia == val;
 0189                    }
 0190                    catch
 0191                    {
 0192                        return false;
 0193                    }
 0194                }).ToList();
 195            }
 196
 1197            if (query.SupportsMediaDeletion.HasValue)
 198            {
 0199                var val = query.SupportsMediaDeletion.Value;
 0200                channels = channels.Where(i =>
 0201                {
 0202                    try
 0203                    {
 0204                        return GetChannelProvider(i) is ISupportsDelete == val;
 0205                    }
 0206                    catch
 0207                    {
 0208                        return false;
 0209                    }
 0210                }).ToList();
 211            }
 212
 1213            if (query.IsFavorite.HasValue)
 214            {
 0215                var val = query.IsFavorite.Value;
 0216                channels = channels.Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val)
 0217                    .ToList();
 218            }
 219
 1220            if (user is not null)
 221            {
 1222                var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
 1223                channels = channels.Where(i =>
 1224                {
 1225                    if (!i.IsVisible(user))
 1226                    {
 1227                        return false;
 1228                    }
 1229
 1230                    try
 1231                    {
 1232                        return GetChannelProvider(i).IsEnabledFor(userId);
 1233                    }
 1234                    catch
 1235                    {
 1236                        return false;
 1237                    }
 1238                }).ToList();
 239            }
 240
 1241            var all = channels;
 1242            var totalCount = all.Count;
 243
 1244            int startIndex = query.StartIndex ?? 0;
 1245            int count = (query.Limit ?? 0) > 0 ? Math.Min(query.Limit.Value, totalCount - startIndex) : totalCount - sta
 1246            all = all.GetRange(query.StartIndex ?? 0, count);
 247
 1248            if (query.RefreshLatestChannelItems)
 249            {
 0250                foreach (var item in all)
 251                {
 0252                    await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(fal
 253                }
 254            }
 255
 1256            return new QueryResult<Channel>(
 1257                query.StartIndex,
 1258                totalCount,
 1259                all);
 1260        }
 261
 262        /// <inheritdoc />
 263        public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
 264        {
 0265            var user = query.UserId.IsEmpty()
 0266                ? null
 0267                : _userManager.GetUserById(query.UserId);
 268
 0269            var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
 270
 0271            var dtoOptions = new DtoOptions();
 272
 273            // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can gener
 0274            var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
 275
 0276            var result = new QueryResult<BaseItemDto>(
 0277                query.StartIndex,
 0278                internalResult.TotalRecordCount,
 0279                returnItems);
 280
 0281            return result;
 0282        }
 283
 284        /// <summary>
 285        /// Refreshes the associated channels.
 286        /// </summary>
 287        /// <param name="progress">The progress.</param>
 288        /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
 289        /// <returns>The completed task.</returns>
 290        public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken)
 291        {
 0292            var allChannelsList = GetAllChannels().ToList();
 293
 0294            var numComplete = 0;
 295
 0296            foreach (var channelInfo in allChannelsList)
 297            {
 0298                cancellationToken.ThrowIfCancellationRequested();
 299
 300                try
 301                {
 0302                    await GetChannel(channelInfo, cancellationToken).ConfigureAwait(false);
 0303                }
 0304                catch (OperationCanceledException)
 305                {
 0306                    throw;
 307                }
 0308                catch (Exception ex)
 309                {
 0310                    _logger.LogError(ex, "Error getting channel information for {0}", channelInfo.Name);
 0311                }
 312
 0313                numComplete++;
 0314                double percent = (double)numComplete / allChannelsList.Count;
 0315                progress.Report(100 * percent);
 0316            }
 317
 0318            progress.Report(100);
 0319        }
 320
 321        private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
 322        {
 2323            foreach (IChannel channel in GetAllChannels())
 324            {
 0325                yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationTok
 326            }
 1327        }
 328
 329        private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
 330        {
 0331            var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
 332
 333            try
 334            {
 0335                var bytes = File.ReadAllBytes(path);
 0336                return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
 0337                    ?? Array.Empty<MediaSourceInfo>();
 338            }
 0339            catch
 340            {
 0341                return Array.Empty<MediaSourceInfo>();
 342            }
 0343        }
 344
 345        private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
 346        {
 0347            var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
 348
 0349            if (mediaSources is null || mediaSources.Count == 0)
 350            {
 351                try
 352                {
 0353                    _fileSystem.DeleteFile(path);
 0354                }
 0355                catch
 356                {
 0357                }
 358
 0359                return;
 360            }
 361
 0362            Directory.CreateDirectory(Path.GetDirectoryName(path));
 363
 0364            FileStream createStream = AsyncFile.Create(path);
 0365            await using (createStream.ConfigureAwait(false))
 366            {
 0367                await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
 368            }
 0369        }
 370
 371        /// <inheritdoc />
 372        public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken)
 373        {
 0374            IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item);
 375
 0376            return results
 0377                .Select(i => NormalizeMediaSource(item, i))
 0378                .ToList();
 379        }
 380
 381        /// <summary>
 382        /// Gets the dynamic media sources based on the provided item.
 383        /// </summary>
 384        /// <param name="item">The item.</param>
 385        /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
 386        /// <returns>The task representing the operation to get the media sources.</returns>
 387        public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancella
 388        {
 0389            var channel = GetChannel(item.ChannelId);
 0390            var channelPlugin = GetChannelProvider(channel);
 391
 392            IEnumerable<MediaSourceInfo> results;
 393
 0394            if (channelPlugin is IRequiresMediaInfoCallback requiresCallback)
 395            {
 0396                results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
 0397                    .ConfigureAwait(false);
 398            }
 399            else
 400            {
 0401                results = Enumerable.Empty<MediaSourceInfo>();
 402            }
 403
 0404            return results
 0405                .Select(i => NormalizeMediaSource(item, i))
 0406                .ToList();
 0407        }
 408
 409        private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback c
 410        {
 0411            if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
 412            {
 0413                return cachedInfo;
 414            }
 415
 0416            var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
 0417                   .ConfigureAwait(false);
 0418            var list = mediaInfo.ToList();
 0419            _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
 420
 0421            return list;
 0422        }
 423
 424        private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info)
 425        {
 0426            info.RunTimeTicks ??= item.RunTimeTicks;
 427
 0428            return info;
 429        }
 430
 431        private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken)
 432        {
 0433            var parentFolderId = Guid.Empty;
 434
 0435            var id = GetInternalChannelId(channelInfo.Name);
 436
 0437            var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id);
 438
 0439            var isNew = false;
 0440            var forceUpdate = false;
 441
 0442            var item = _libraryManager.GetItemById(id) as Channel;
 443
 0444            if (item is null)
 445            {
 0446                var info = Directory.CreateDirectory(path);
 0447                item = new Channel
 0448                {
 0449                    Name = channelInfo.Name,
 0450                    Id = id,
 0451                    DateCreated = info.CreationTimeUtc,
 0452                    DateModified = info.LastWriteTimeUtc
 0453                };
 454
 0455                isNew = true;
 456            }
 457
 0458            if (!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 459            {
 0460                isNew = true;
 461            }
 462
 0463            item.Path = path;
 464
 0465            if (!item.ChannelId.Equals(id))
 466            {
 0467                forceUpdate = true;
 468            }
 469
 0470            item.ChannelId = id;
 471
 0472            if (!item.ParentId.Equals(parentFolderId))
 473            {
 0474                forceUpdate = true;
 475            }
 476
 0477            item.ParentId = parentFolderId;
 478
 0479            item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
 0480            item.Overview = channelInfo.Description;
 481
 0482            if (string.IsNullOrWhiteSpace(item.Name))
 483            {
 0484                item.Name = channelInfo.Name;
 485            }
 486
 0487            if (isNew)
 488            {
 0489                item.OnMetadataChanged();
 0490                _libraryManager.CreateItem(item, null);
 491            }
 492
 0493            await item.RefreshMetadata(
 0494                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0495                {
 0496                    ForceSave = !isNew && forceUpdate
 0497                },
 0498                cancellationToken).ConfigureAwait(false);
 499
 0500            return item;
 0501        }
 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        {
 0594            var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false);
 595
 0596            var items = internalResult.Items;
 0597            var totalRecordCount = internalResult.TotalRecordCount;
 598
 0599            var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User);
 600
 0601            var result = new QueryResult<BaseItemDto>(
 0602                query.StartIndex,
 0603                totalRecordCount,
 0604                returnItems);
 605
 0606            return result;
 0607        }
 608
 609        /// <inheritdoc />
 610        public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationTok
 611        {
 0612            var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
 613
 0614            if (query.ChannelIds.Count > 0)
 615            {
 616                // Avoid implicitly captured closure
 0617                var ids = query.ChannelIds;
 0618                channels = channels
 0619                    .Where(i => ids.Contains(GetInternalChannelId(i.Name)))
 0620                    .ToArray();
 621            }
 622
 0623            if (channels.Length == 0)
 624            {
 0625                return new QueryResult<BaseItem>();
 626            }
 627
 0628            foreach (var channel in channels)
 629            {
 0630                await RefreshLatestChannelItems(channel, cancellationToken).ConfigureAwait(false);
 631            }
 632
 0633            query.IsFolder = false;
 634
 635            // hack for trailers, figure out a better way later
 0636            var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringCompar
 637
 0638            if (sortByPremiereDate)
 639            {
 0640                query.OrderBy = new[]
 0641                {
 0642                    (ItemSortBy.PremiereDate, SortOrder.Descending),
 0643                    (ItemSortBy.ProductionYear, SortOrder.Descending),
 0644                    (ItemSortBy.DateCreated, SortOrder.Descending)
 0645                };
 646            }
 647            else
 648            {
 0649                query.OrderBy = new[]
 0650                {
 0651                    (ItemSortBy.DateCreated, SortOrder.Descending)
 0652                };
 653            }
 654
 0655            return _libraryManager.GetItemsResult(query);
 0656        }
 657
 658        private async Task RefreshLatestChannelItems(IChannel channel, CancellationToken cancellationToken)
 659        {
 0660            var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
 661
 0662            var query = new InternalItemsQuery
 0663            {
 0664                Parent = internalChannel,
 0665                EnableTotalRecordCount = false,
 0666                ChannelIds = new Guid[] { internalChannel.Id }
 0667            };
 668
 0669            var result = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).ConfigureAwait(
 670
 0671            foreach (var item in result.Items)
 672            {
 0673                if (item is Folder folder)
 674                {
 0675                    await GetChannelItemsInternal(
 0676                        new InternalItemsQuery
 0677                        {
 0678                            Parent = folder,
 0679                            EnableTotalRecordCount = false,
 0680                            ChannelIds = new Guid[] { internalChannel.Id }
 0681                        },
 0682                        new Progress<double>(),
 0683                        cancellationToken).ConfigureAwait(false);
 684                }
 685            }
 0686        }
 687
 688        /// <inheritdoc />
 689        public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> pro
 690        {
 691            // Get the internal channel entity
 0692            var channel = GetChannel(query.ChannelIds[0]);
 693
 694            // Find the corresponding channel provider plugin
 0695            var channelProvider = GetChannelProvider(channel);
 696
 0697            var parentItem = query.ParentId.IsEmpty()
 0698                ? channel
 0699                : _libraryManager.GetItemById(query.ParentId);
 700
 0701            var itemsResult = await GetChannelItems(
 0702                channelProvider,
 0703                query.User,
 0704                parentItem is Channel ? null : parentItem.ExternalId,
 0705                null,
 0706                false,
 0707                cancellationToken)
 0708                .ConfigureAwait(false);
 709
 0710            if (query.ParentId.IsEmpty())
 711            {
 0712                query.Parent = channel;
 713            }
 714
 0715            query.ChannelIds = Array.Empty<Guid>();
 716
 717            // Not yet sure why this is causing a problem
 0718            query.GroupByPresentationUniqueKey = false;
 719
 720            // null if came from cache
 0721            if (itemsResult is not null)
 722            {
 0723                var items = itemsResult.Items;
 0724                var itemsLen = items.Count;
 0725                var internalItems = new Guid[itemsLen];
 0726                for (int i = 0; i < itemsLen; i++)
 727                {
 0728                    internalItems[i] = (await GetChannelItemEntityAsync(
 0729                        items[i],
 0730                        channelProvider,
 0731                        channel.Id,
 0732                        parentItem,
 0733                        cancellationToken).ConfigureAwait(false)).Id;
 734                }
 735
 0736                var existingIds = _libraryManager.GetItemIds(query);
 0737                var deadIds = existingIds.Except(internalItems)
 0738                    .ToArray();
 739
 0740                foreach (var deadId in deadIds)
 741                {
 0742                    var deadItem = _libraryManager.GetItemById(deadId);
 0743                    if (deadItem is not null)
 744                    {
 0745                        _libraryManager.DeleteItem(
 0746                            deadItem,
 0747                            new DeleteOptions
 0748                            {
 0749                                DeleteFileLocation = false,
 0750                                DeleteFromExternalProvider = false
 0751                            },
 0752                            parentItem,
 0753                            false);
 754                    }
 755                }
 0756            }
 757
 0758            return _libraryManager.GetItemsResult(query);
 0759        }
 760
 761        /// <inheritdoc />
 762        public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancella
 763        {
 0764            var internalResult = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).Configu
 765
 0766            var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User);
 767
 0768            var result = new QueryResult<BaseItemDto>(
 0769                query.StartIndex,
 0770                internalResult.TotalRecordCount,
 0771                returnItems);
 772
 0773            return result;
 0774        }
 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        {
 0784            var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
 785
 0786            var cacheLength = CacheLength;
 0787            var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
 788
 789            try
 790            {
 0791                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
 792                {
 0793                    var jsonStream = AsyncFile.OpenRead(cachePath);
 0794                    await using (jsonStream.ConfigureAwait(false))
 795                    {
 0796                        var cachedResult = await JsonSerializer
 0797                            .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
 0798                            .ConfigureAwait(false);
 0799                        if (cachedResult is not null)
 800                        {
 0801                            return null;
 802                        }
 803                    }
 804                }
 0805            }
 0806            catch (FileNotFoundException)
 807            {
 0808            }
 0809            catch (IOException)
 810            {
 0811            }
 812
 0813            using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 814            {
 815                try
 816                {
 0817                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
 818                    {
 0819                        var jsonStream = AsyncFile.OpenRead(cachePath);
 0820                        await using (jsonStream.ConfigureAwait(false))
 821                        {
 0822                            var cachedResult = await JsonSerializer
 0823                                .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
 0824                                .ConfigureAwait(false);
 0825                            if (cachedResult is not null)
 826                            {
 0827                                return null;
 828                            }
 829                        }
 830                    }
 0831                }
 0832                catch (FileNotFoundException)
 833                {
 0834                }
 0835                catch (IOException)
 836                {
 0837                }
 838
 0839                var query = new InternalChannelItemQuery
 0840                {
 0841                    UserId = user?.Id ?? Guid.Empty,
 0842                    SortBy = sortField,
 0843                    SortDescending = sortDescending,
 0844                    FolderId = externalFolderId
 0845                };
 846
 0847                query.FolderId = externalFolderId;
 848
 0849                var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false);
 850
 0851                if (result is null)
 852                {
 0853                    throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
 854                }
 855
 0856                await CacheResponse(result, cachePath).ConfigureAwait(false);
 857
 0858                return result;
 859            }
 0860        }
 861
 862        private async Task CacheResponse(ChannelItemResult result, string path)
 863        {
 864            try
 865            {
 0866                Directory.CreateDirectory(Path.GetDirectoryName(path));
 867
 0868                var createStream = AsyncFile.Create(path);
 0869                await using (createStream.ConfigureAwait(false))
 870                {
 0871                    await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
 872                }
 0873            }
 0874            catch (Exception ex)
 875            {
 0876                _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
 0877            }
 0878        }
 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        {
 0960            var parentFolderId = parentFolder.Id;
 961
 962            BaseItem item;
 963            bool isNew;
 0964            bool forceUpdate = false;
 965
 0966            if (info.Type == ChannelItemType.Folder)
 967            {
 0968                item = info.FolderType switch
 0969                {
 0970                    ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew),
 0971                    ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew),
 0972                    ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew),
 0973                    ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew),
 0974                    ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew),
 0975                    _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew)
 0976                };
 977            }
 0978            else if (info.MediaType == ChannelMediaType.Audio)
 979            {
 0980                item = info.ContentType == ChannelMediaContentType.Podcast
 0981                    ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew)
 0982                    : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew);
 983            }
 984            else
 985            {
 0986                item = info.ContentType switch
 0987                {
 0988                    ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew),
 0989                    ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew),
 0990                    var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer
 0991                    => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew),
 0992                    _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew)
 0993                };
 994            }
 995
 0996            var enableMediaProbe = channelProvider is ISupportsMediaProbe;
 997
 0998            if (info.IsLiveStream)
 999            {
 01000                item.RunTimeTicks = null;
 1001            }
 01002            else if (isNew || !enableMediaProbe)
 1003            {
 01004                item.RunTimeTicks = info.RunTimeTicks;
 1005            }
 1006
 01007            if (isNew)
 1008            {
 01009                item.Name = info.Name;
 01010                item.Genres = info.Genres.ToArray();
 01011                item.Studios = info.Studios.ToArray();
 01012                item.CommunityRating = info.CommunityRating;
 01013                item.Overview = info.Overview;
 01014                item.IndexNumber = info.IndexNumber;
 01015                item.ParentIndexNumber = info.ParentIndexNumber;
 01016                item.PremiereDate = info.PremiereDate;
 01017                item.ProductionYear = info.ProductionYear;
 01018                item.ProviderIds = info.ProviderIds;
 01019                item.OfficialRating = info.OfficialRating;
 01020                item.DateCreated = info.DateCreated ?? DateTime.UtcNow;
 01021                item.Tags = info.Tags.ToArray();
 01022                item.OriginalTitle = info.OriginalTitle;
 1023            }
 01024            else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container)
 1025            {
 1026                // At least update names of container folders
 01027                if (item.Name != info.Name)
 1028                {
 01029                    item.Name = info.Name;
 01030                    forceUpdate = true;
 1031                }
 1032            }
 1033
 01034            if (item is IHasArtist hasArtists)
 1035            {
 01036                hasArtists.Artists = info.Artists.ToArray();
 1037            }
 1038
 01039            if (item is IHasAlbumArtist hasAlbumArtists)
 1040            {
 01041                hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
 1042            }
 1043
 01044            if (item is Trailer trailer)
 1045            {
 01046                if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
 1047                {
 01048                    _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
 01049                    forceUpdate = true;
 1050                }
 1051
 01052                trailer.TrailerTypes = info.TrailerTypes.ToArray();
 1053            }
 1054
 01055            if (info.DateModified > item.DateModified)
 1056            {
 01057                item.DateModified = info.DateModified;
 01058                _logger.LogDebug("Forcing update due to DateModified {0}", item.Name);
 01059                forceUpdate = true;
 1060            }
 1061
 01062            if (!internalChannelId.Equals(item.ChannelId))
 1063            {
 01064                forceUpdate = true;
 01065                _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
 1066            }
 1067
 01068            item.ChannelId = internalChannelId;
 1069
 01070            if (!item.ParentId.Equals(parentFolderId))
 1071            {
 01072                forceUpdate = true;
 01073                _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
 1074            }
 1075
 01076            item.ParentId = parentFolderId;
 1077
 01078            if (item is IHasSeries hasSeries)
 1079            {
 01080                if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
 1081                {
 01082                    forceUpdate = true;
 01083                    _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
 1084                }
 1085
 01086                hasSeries.SeriesName = info.SeriesName;
 1087            }
 1088
 01089            if (!string.Equals(item.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase))
 1090            {
 01091                forceUpdate = true;
 01092                _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
 1093            }
 1094
 01095            item.ExternalId = info.Id;
 1096
 01097            if (item is Audio channelAudioItem)
 1098            {
 01099                channelAudioItem.ExtraType = info.ExtraType;
 1100
 01101                var mediaSource = info.MediaSources.FirstOrDefault();
 01102                item.Path = mediaSource?.Path;
 1103            }
 1104
 01105            if (item is Video channelVideoItem)
 1106            {
 01107                channelVideoItem.ExtraType = info.ExtraType;
 1108
 01109                var mediaSource = info.MediaSources.FirstOrDefault();
 01110                item.Path = mediaSource?.Path;
 1111            }
 1112
 01113            if (LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(item, null, info.ImageUrl))
 1114            {
 01115                _logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name);
 01116                forceUpdate = true;
 1117            }
 1118
 01119            if (!info.IsLiveStream)
 1120            {
 01121                if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
 1122                {
 01123                    item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
 01124                    _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
 01125                    forceUpdate = true;
 1126                }
 1127            }
 1128            else
 1129            {
 01130                if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
 1131                {
 01132                    item.Tags = [.. item.Tags, "livestream"];
 01133                    _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
 01134                    forceUpdate = true;
 1135                }
 1136            }
 1137
 01138            item.OnMetadataChanged();
 1139
 01140            if (isNew)
 1141            {
 01142                _libraryManager.CreateItem(item, parentFolder);
 1143
 01144                if (info.People is not null && info.People.Count > 0)
 1145                {
 01146                    await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
 1147                }
 1148            }
 01149            else if (forceUpdate)
 1150            {
 01151                await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
 1152            }
 1153
 01154            if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
 1155            {
 01156                if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
 1157                {
 01158                    await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
 1159                }
 1160                else
 1161                {
 01162                    await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
 1163                }
 1164            }
 1165
 01166            if (isNew || forceUpdate || item.DateLastRefreshed == DateTime.MinValue)
 1167            {
 01168                _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Re
 1169            }
 1170
 01171            return item;
 01172        }
 1173
 1174        internal IChannel GetChannelProvider(Channel channel)
 1175        {
 01176            ArgumentNullException.ThrowIfNull(channel);
 1177
 01178            var result = GetAllChannels()
 01179                .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, cha
 1180
 01181            if (result is null)
 1182            {
 01183                throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name);
 1184            }
 1185
 01186            return result;
 1187        }
 1188
 1189        /// <inheritdoc />
 1190        public void Dispose()
 1191        {
 221192            Dispose(true);
 221193            GC.SuppressFinalize(this);
 221194        }
 1195
 1196        /// <summary>
 1197        /// Releases unmanaged and optionally managed resources.
 1198        /// </summary>
 1199        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 1200        protected virtual void Dispose(bool disposing)
 1201        {
 221202            if (_disposed)
 1203            {
 01204                return;
 1205            }
 1206
 221207            if (disposing)
 1208            {
 221209                _resourcePool?.Dispose();
 1210            }
 1211
 221212            _disposed = true;
 221213        }
 1214    }
 1215}

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()
GetChannelsInternalAsync()
GetChannelsAsync()
RefreshChannels()
GetAllChannelEntitiesAsync()
GetSavedMediaSources(MediaBrowser.Controller.Entities.BaseItem)
SaveMediaSources()
GetStaticMediaSources(MediaBrowser.Controller.Entities.BaseItem,System.Threading.CancellationToken)
GetDynamicMediaSources()
GetChannelItemMediaSourcesInternal()
NormalizeMediaSource(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceInfo)
GetChannel()
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)
GetLatestChannelItems()
GetLatestChannelItemsInternal()
RefreshLatestChannelItems()
GetChannelItemsInternal()
GetChannelItems()
GetChannelItems()
CacheResponse()
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&)
GetChannelItemEntityAsync()
GetChannelProvider(MediaBrowser.Controller.Channels.Channel)
Dispose()
Dispose(System.Boolean)