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

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)