< 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: 1217
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 = File.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                item = new Channel
 449                {
 450                    Name = channelInfo.Name,
 451                    Id = id,
 452                    DateCreated = _fileSystem.GetCreationTimeUtc(path),
 453                    DateModified = _fileSystem.GetLastWriteTimeUtc(path)
 454                };
 455
 456                isNew = true;
 457            }
 458
 459            if (!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 460            {
 461                isNew = true;
 462            }
 463
 464            item.Path = path;
 465
 466            if (!item.ChannelId.Equals(id))
 467            {
 468                forceUpdate = true;
 469            }
 470
 471            item.ChannelId = id;
 472
 473            if (!item.ParentId.Equals(parentFolderId))
 474            {
 475                forceUpdate = true;
 476            }
 477
 478            item.ParentId = parentFolderId;
 479
 480            item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
 481            item.Overview = channelInfo.Description;
 482
 483            if (string.IsNullOrWhiteSpace(item.Name))
 484            {
 485                item.Name = channelInfo.Name;
 486            }
 487
 488            if (isNew)
 489            {
 490                item.OnMetadataChanged();
 491                _libraryManager.CreateItem(item, null);
 492            }
 493
 494            await item.RefreshMetadata(
 495                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 496                {
 497                    ForceSave = !isNew && forceUpdate
 498                },
 499                cancellationToken).ConfigureAwait(false);
 500
 501            return item;
 502        }
 503
 504        private static string GetOfficialRating(ChannelParentalRating rating)
 505        {
 0506            return rating switch
 0507            {
 0508                ChannelParentalRating.Adult => "XXX",
 0509                ChannelParentalRating.UsR => "R",
 0510                ChannelParentalRating.UsPG13 => "PG-13",
 0511                ChannelParentalRating.UsPG => "PG",
 0512                _ => null
 0513            };
 514        }
 515
 516        /// <summary>
 517        /// Gets a channel with the provided Guid.
 518        /// </summary>
 519        /// <param name="id">The Guid.</param>
 520        /// <returns>The corresponding channel.</returns>
 521        public Channel GetChannel(Guid id)
 522        {
 0523            return _libraryManager.GetItemById(id) as Channel;
 524        }
 525
 526        /// <inheritdoc />
 527        public Channel GetChannel(string id)
 528        {
 0529            return _libraryManager.GetItemById(id) as Channel;
 530        }
 531
 532        /// <inheritdoc />
 533        public ChannelFeatures[] GetAllChannelFeatures()
 534        {
 0535            return _libraryManager.GetItemIds(
 0536                new InternalItemsQuery
 0537                {
 0538                    IncludeItemTypes = new[] { BaseItemKind.Channel },
 0539                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 0540                }).Select(i => GetChannelFeatures(i)).ToArray();
 541        }
 542
 543        /// <inheritdoc />
 544        public ChannelFeatures GetChannelFeatures(Guid? id)
 545        {
 0546            if (!id.HasValue)
 547            {
 0548                throw new ArgumentNullException(nameof(id));
 549            }
 550
 0551            var channel = GetChannel(id.Value);
 0552            var channelProvider = GetChannelProvider(channel);
 553
 0554            return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures());
 555        }
 556
 557        /// <summary>
 558        /// Gets the provided channel's supported features.
 559        /// </summary>
 560        /// <param name="channel">The channel.</param>
 561        /// <param name="provider">The provider.</param>
 562        /// <param name="features">The features.</param>
 563        /// <returns>The supported features.</returns>
 564        public ChannelFeatures GetChannelFeaturesDto(
 565            Channel channel,
 566            IChannel provider,
 567            InternalChannelFeatures features)
 568        {
 0569            var supportsLatest = provider is ISupportsLatestMedia;
 570
 0571            return new ChannelFeatures(channel.Name, channel.Id)
 0572            {
 0573                CanFilter = !features.MaxPageSize.HasValue,
 0574                ContentTypes = features.ContentTypes.ToArray(),
 0575                DefaultSortFields = features.DefaultSortFields.ToArray(),
 0576                MaxPageSize = features.MaxPageSize,
 0577                MediaTypes = features.MediaTypes.ToArray(),
 0578                SupportsSortOrderToggle = features.SupportsSortOrderToggle,
 0579                SupportsLatestMedia = supportsLatest,
 0580                SupportsContentDownloading = features.SupportsContentDownloading,
 0581                AutoRefreshLevels = features.AutoRefreshLevels
 0582            };
 583        }
 584
 585        private Guid GetInternalChannelId(string name)
 586        {
 0587            ArgumentException.ThrowIfNullOrEmpty(name);
 588
 0589            return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
 590        }
 591
 592        /// <inheritdoc />
 593        public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken ca
 594        {
 595            var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false);
 596
 597            var items = internalResult.Items;
 598            var totalRecordCount = internalResult.TotalRecordCount;
 599
 600            var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User);
 601
 602            var result = new QueryResult<BaseItemDto>(
 603                query.StartIndex,
 604                totalRecordCount,
 605                returnItems);
 606
 607            return result;
 608        }
 609
 610        /// <inheritdoc />
 611        public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationTok
 612        {
 613            var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
 614
 615            if (query.ChannelIds.Count > 0)
 616            {
 617                // Avoid implicitly captured closure
 618                var ids = query.ChannelIds;
 619                channels = channels
 620                    .Where(i => ids.Contains(GetInternalChannelId(i.Name)))
 621                    .ToArray();
 622            }
 623
 624            if (channels.Length == 0)
 625            {
 626                return new QueryResult<BaseItem>();
 627            }
 628
 629            foreach (var channel in channels)
 630            {
 631                await RefreshLatestChannelItems(channel, cancellationToken).ConfigureAwait(false);
 632            }
 633
 634            query.IsFolder = false;
 635
 636            // hack for trailers, figure out a better way later
 637            var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringCompar
 638
 639            if (sortByPremiereDate)
 640            {
 641                query.OrderBy = new[]
 642                {
 643                    (ItemSortBy.PremiereDate, SortOrder.Descending),
 644                    (ItemSortBy.ProductionYear, SortOrder.Descending),
 645                    (ItemSortBy.DateCreated, SortOrder.Descending)
 646                };
 647            }
 648            else
 649            {
 650                query.OrderBy = new[]
 651                {
 652                    (ItemSortBy.DateCreated, SortOrder.Descending)
 653                };
 654            }
 655
 656            return _libraryManager.GetItemsResult(query);
 657        }
 658
 659        private async Task RefreshLatestChannelItems(IChannel channel, CancellationToken cancellationToken)
 660        {
 661            var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
 662
 663            var query = new InternalItemsQuery
 664            {
 665                Parent = internalChannel,
 666                EnableTotalRecordCount = false,
 667                ChannelIds = new Guid[] { internalChannel.Id }
 668            };
 669
 670            var result = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).ConfigureAwait(
 671
 672            foreach (var item in result.Items)
 673            {
 674                if (item is Folder folder)
 675                {
 676                    await GetChannelItemsInternal(
 677                        new InternalItemsQuery
 678                        {
 679                            Parent = folder,
 680                            EnableTotalRecordCount = false,
 681                            ChannelIds = new Guid[] { internalChannel.Id }
 682                        },
 683                        new Progress<double>(),
 684                        cancellationToken).ConfigureAwait(false);
 685                }
 686            }
 687        }
 688
 689        /// <inheritdoc />
 690        public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> pro
 691        {
 692            // Get the internal channel entity
 693            var channel = GetChannel(query.ChannelIds[0]);
 694
 695            // Find the corresponding channel provider plugin
 696            var channelProvider = GetChannelProvider(channel);
 697
 698            var parentItem = query.ParentId.IsEmpty()
 699                ? channel
 700                : _libraryManager.GetItemById(query.ParentId);
 701
 702            var itemsResult = await GetChannelItems(
 703                channelProvider,
 704                query.User,
 705                parentItem is Channel ? null : parentItem.ExternalId,
 706                null,
 707                false,
 708                cancellationToken)
 709                .ConfigureAwait(false);
 710
 711            if (query.ParentId.IsEmpty())
 712            {
 713                query.Parent = channel;
 714            }
 715
 716            query.ChannelIds = Array.Empty<Guid>();
 717
 718            // Not yet sure why this is causing a problem
 719            query.GroupByPresentationUniqueKey = false;
 720
 721            // null if came from cache
 722            if (itemsResult is not null)
 723            {
 724                var items = itemsResult.Items;
 725                var itemsLen = items.Count;
 726                var internalItems = new Guid[itemsLen];
 727                for (int i = 0; i < itemsLen; i++)
 728                {
 729                    internalItems[i] = (await GetChannelItemEntityAsync(
 730                        items[i],
 731                        channelProvider,
 732                        channel.Id,
 733                        parentItem,
 734                        cancellationToken).ConfigureAwait(false)).Id;
 735                }
 736
 737                var existingIds = _libraryManager.GetItemIds(query);
 738                var deadIds = existingIds.Except(internalItems)
 739                    .ToArray();
 740
 741                foreach (var deadId in deadIds)
 742                {
 743                    var deadItem = _libraryManager.GetItemById(deadId);
 744                    if (deadItem is not null)
 745                    {
 746                        _libraryManager.DeleteItem(
 747                            deadItem,
 748                            new DeleteOptions
 749                            {
 750                                DeleteFileLocation = false,
 751                                DeleteFromExternalProvider = false
 752                            },
 753                            parentItem,
 754                            false);
 755                    }
 756                }
 757            }
 758
 759            return _libraryManager.GetItemsResult(query);
 760        }
 761
 762        /// <inheritdoc />
 763        public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancella
 764        {
 765            var internalResult = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).Configu
 766
 767            var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User);
 768
 769            var result = new QueryResult<BaseItemDto>(
 770                query.StartIndex,
 771                internalResult.TotalRecordCount,
 772                returnItems);
 773
 774            return result;
 775        }
 776
 777        private async Task<ChannelItemResult> GetChannelItems(
 778            IChannel channel,
 779            User user,
 780            string externalFolderId,
 781            ChannelItemSortField? sortField,
 782            bool sortDescending,
 783            CancellationToken cancellationToken)
 784        {
 785            var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
 786
 787            var cacheLength = CacheLength;
 788            var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
 789
 790            try
 791            {
 792                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
 793                {
 794                    var jsonStream = AsyncFile.OpenRead(cachePath);
 795                    await using (jsonStream.ConfigureAwait(false))
 796                    {
 797                        var cachedResult = await JsonSerializer
 798                            .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
 799                            .ConfigureAwait(false);
 800                        if (cachedResult is not null)
 801                        {
 802                            return null;
 803                        }
 804                    }
 805                }
 806            }
 807            catch (FileNotFoundException)
 808            {
 809            }
 810            catch (IOException)
 811            {
 812            }
 813
 814            using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 815            {
 816                try
 817                {
 818                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
 819                    {
 820                        var jsonStream = AsyncFile.OpenRead(cachePath);
 821                        await using (jsonStream.ConfigureAwait(false))
 822                        {
 823                            var cachedResult = await JsonSerializer
 824                                .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
 825                                .ConfigureAwait(false);
 826                            if (cachedResult is not null)
 827                            {
 828                                return null;
 829                            }
 830                        }
 831                    }
 832                }
 833                catch (FileNotFoundException)
 834                {
 835                }
 836                catch (IOException)
 837                {
 838                }
 839
 840                var query = new InternalChannelItemQuery
 841                {
 842                    UserId = user?.Id ?? Guid.Empty,
 843                    SortBy = sortField,
 844                    SortDescending = sortDescending,
 845                    FolderId = externalFolderId
 846                };
 847
 848                query.FolderId = externalFolderId;
 849
 850                var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false);
 851
 852                if (result is null)
 853                {
 854                    throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
 855                }
 856
 857                await CacheResponse(result, cachePath).ConfigureAwait(false);
 858
 859                return result;
 860            }
 861        }
 862
 863        private async Task CacheResponse(ChannelItemResult result, string path)
 864        {
 865            try
 866            {
 867                Directory.CreateDirectory(Path.GetDirectoryName(path));
 868
 869                var createStream = File.Create(path);
 870                await using (createStream.ConfigureAwait(false))
 871                {
 872                    await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
 873                }
 874            }
 875            catch (Exception ex)
 876            {
 877                _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
 878            }
 879        }
 880
 881        private string GetChannelDataCachePath(
 882            IChannel channel,
 883            string userId,
 884            string externalFolderId,
 885            ChannelItemSortField? sortField,
 886            bool sortDescending)
 887        {
 0888            var channelId = GetInternalChannelId(channel.Name).ToString("N", CultureInfo.InvariantCulture);
 889
 0890            var userCacheKey = string.Empty;
 891
 0892            if (channel is IHasCacheKey hasCacheKey)
 893            {
 0894                userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty;
 895            }
 896
 0897            var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N", Cul
 0898            filename += userCacheKey;
 899
 0900            var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N", CultureInfo.InvariantCult
 901
 0902            if (sortField.HasValue)
 903            {
 0904                filename += "-sortField-" + sortField.Value;
 905            }
 906
 0907            if (sortDescending)
 908            {
 0909                filename += "-sortDescending";
 910            }
 911
 0912            filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 913
 0914            return Path.Combine(
 0915                _config.ApplicationPaths.CachePath,
 0916                "channels",
 0917                channelId,
 0918                version,
 0919                filename + ".json");
 920        }
 921
 922        private static string GetIdToHash(string externalId, string channelName)
 923        {
 924            // Increment this as needed to force new downloads
 925            // Incorporate Name because it's being used to convert channel entity to provider
 0926            return externalId + (channelName ?? string.Empty) + "16";
 927        }
 928
 929        private T GetItemById<T>(string idString, string channelName, out bool isNew)
 930            where T : BaseItem, new()
 931        {
 0932            var id = _libraryManager.GetNewItemId(GetIdToHash(idString, channelName), typeof(T));
 933
 0934            T item = null;
 935
 936            try
 937            {
 0938                item = _libraryManager.GetItemById(id) as T;
 0939            }
 0940            catch (Exception ex)
 941            {
 0942                _logger.LogError(ex, "Error retrieving channel item from database");
 0943            }
 944
 0945            if (item is null)
 946            {
 0947                item = new T();
 0948                isNew = true;
 949            }
 950            else
 951            {
 0952                isNew = false;
 953            }
 954
 0955            item.Id = id;
 0956            return item;
 957        }
 958
 959        private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid inte
 960        {
 961            var parentFolderId = parentFolder.Id;
 962
 963            BaseItem item;
 964            bool isNew;
 965            bool forceUpdate = false;
 966
 967            if (info.Type == ChannelItemType.Folder)
 968            {
 969                item = info.FolderType switch
 970                {
 971                    ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew),
 972                    ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew),
 973                    ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew),
 974                    ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew),
 975                    ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew),
 976                    _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew)
 977                };
 978            }
 979            else if (info.MediaType == ChannelMediaType.Audio)
 980            {
 981                item = info.ContentType == ChannelMediaContentType.Podcast
 982                    ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew)
 983                    : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew);
 984            }
 985            else
 986            {
 987                item = info.ContentType switch
 988                {
 989                    ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew),
 990                    ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew),
 991                    var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer
 992                    => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew),
 993                    _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew)
 994                };
 995            }
 996
 997            var enableMediaProbe = channelProvider is ISupportsMediaProbe;
 998
 999            if (info.IsLiveStream)
 1000            {
 1001                item.RunTimeTicks = null;
 1002            }
 1003            else if (isNew || !enableMediaProbe)
 1004            {
 1005                item.RunTimeTicks = info.RunTimeTicks;
 1006            }
 1007
 1008            if (isNew)
 1009            {
 1010                item.Name = info.Name;
 1011                item.Genres = info.Genres.ToArray();
 1012                item.Studios = info.Studios.ToArray();
 1013                item.CommunityRating = info.CommunityRating;
 1014                item.Overview = info.Overview;
 1015                item.IndexNumber = info.IndexNumber;
 1016                item.ParentIndexNumber = info.ParentIndexNumber;
 1017                item.PremiereDate = info.PremiereDate;
 1018                item.ProductionYear = info.ProductionYear;
 1019                item.ProviderIds = info.ProviderIds;
 1020                item.OfficialRating = info.OfficialRating;
 1021                item.DateCreated = info.DateCreated ?? DateTime.UtcNow;
 1022                item.Tags = info.Tags.ToArray();
 1023                item.OriginalTitle = info.OriginalTitle;
 1024            }
 1025            else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container)
 1026            {
 1027                // At least update names of container folders
 1028                if (item.Name != info.Name)
 1029                {
 1030                    item.Name = info.Name;
 1031                    forceUpdate = true;
 1032                }
 1033            }
 1034
 1035            if (item is IHasArtist hasArtists)
 1036            {
 1037                hasArtists.Artists = info.Artists.ToArray();
 1038            }
 1039
 1040            if (item is IHasAlbumArtist hasAlbumArtists)
 1041            {
 1042                hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
 1043            }
 1044
 1045            if (item is Trailer trailer)
 1046            {
 1047                if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
 1048                {
 1049                    _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
 1050                    forceUpdate = true;
 1051                }
 1052
 1053                trailer.TrailerTypes = info.TrailerTypes.ToArray();
 1054            }
 1055
 1056            if (info.DateModified > item.DateModified)
 1057            {
 1058                item.DateModified = info.DateModified;
 1059                _logger.LogDebug("Forcing update due to DateModified {0}", item.Name);
 1060                forceUpdate = true;
 1061            }
 1062
 1063            if (!internalChannelId.Equals(item.ChannelId))
 1064            {
 1065                forceUpdate = true;
 1066                _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
 1067            }
 1068
 1069            item.ChannelId = internalChannelId;
 1070
 1071            if (!item.ParentId.Equals(parentFolderId))
 1072            {
 1073                forceUpdate = true;
 1074                _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
 1075            }
 1076
 1077            item.ParentId = parentFolderId;
 1078
 1079            if (item is IHasSeries hasSeries)
 1080            {
 1081                if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
 1082                {
 1083                    forceUpdate = true;
 1084                    _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
 1085                }
 1086
 1087                hasSeries.SeriesName = info.SeriesName;
 1088            }
 1089
 1090            if (!string.Equals(item.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase))
 1091            {
 1092                forceUpdate = true;
 1093                _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
 1094            }
 1095
 1096            item.ExternalId = info.Id;
 1097
 1098            if (item is Audio channelAudioItem)
 1099            {
 1100                channelAudioItem.ExtraType = info.ExtraType;
 1101
 1102                var mediaSource = info.MediaSources.FirstOrDefault();
 1103                item.Path = mediaSource?.Path;
 1104            }
 1105
 1106            if (item is Video channelVideoItem)
 1107            {
 1108                channelVideoItem.ExtraType = info.ExtraType;
 1109
 1110                var mediaSource = info.MediaSources.FirstOrDefault();
 1111                item.Path = mediaSource?.Path;
 1112            }
 1113
 1114            if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
 1115            {
 1116                item.SetImagePath(ImageType.Primary, info.ImageUrl);
 1117                _logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name);
 1118                forceUpdate = true;
 1119            }
 1120
 1121            if (!info.IsLiveStream)
 1122            {
 1123                if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
 1124                {
 1125                    item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
 1126                    _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
 1127                    forceUpdate = true;
 1128                }
 1129            }
 1130            else
 1131            {
 1132                if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
 1133                {
 1134                    item.Tags = [..item.Tags, "livestream"];
 1135                    _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
 1136                    forceUpdate = true;
 1137                }
 1138            }
 1139
 1140            item.OnMetadataChanged();
 1141
 1142            if (isNew)
 1143            {
 1144                _libraryManager.CreateItem(item, parentFolder);
 1145
 1146                if (info.People is not null && info.People.Count > 0)
 1147                {
 1148                    await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
 1149                }
 1150            }
 1151            else if (forceUpdate)
 1152            {
 1153                await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
 1154            }
 1155
 1156            if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
 1157            {
 1158                if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
 1159                {
 1160                    await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
 1161                }
 1162                else
 1163                {
 1164                    await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
 1165                }
 1166            }
 1167
 1168            if (isNew || forceUpdate || item.DateLastRefreshed == default)
 1169            {
 1170                _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Re
 1171            }
 1172
 1173            return item;
 1174        }
 1175
 1176        internal IChannel GetChannelProvider(Channel channel)
 1177        {
 01178            ArgumentNullException.ThrowIfNull(channel);
 1179
 01180            var result = GetAllChannels()
 01181                .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, cha
 1182
 01183            if (result is null)
 1184            {
 01185                throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name);
 1186            }
 1187
 01188            return result;
 1189        }
 1190
 1191        /// <inheritdoc />
 1192        public void Dispose()
 1193        {
 211194            Dispose(true);
 211195            GC.SuppressFinalize(this);
 211196        }
 1197
 1198        /// <summary>
 1199        /// Releases unmanaged and optionally managed resources.
 1200        /// </summary>
 1201        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 1202        protected virtual void Dispose(bool disposing)
 1203        {
 211204            if (_disposed)
 1205            {
 01206                return;
 1207            }
 1208
 211209            if (disposing)
 1210            {
 211211                _resourcePool?.Dispose();
 1212            }
 1213
 211214            _disposed = true;
 211215        }
 1216    }
 1217}

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)