#pragma warning disable CS1591 using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Movie = MediaBrowser.Controller.Entities.Movies.Movie; using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; using Person = MediaBrowser.Controller.Entities.Person; using Photo = MediaBrowser.Controller.Entities.Photo; using Season = MediaBrowser.Controller.Entities.TV.Season; using Series = MediaBrowser.Controller.Entities.TV.Series; namespace Emby.Server.Implementations.Dto { public class DtoService : IDtoService { private static readonly FrozenDictionary _relatedItemKinds = new Dictionary { { BaseItemKind.Genre, [ BaseItemKind.Audio, BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.LiveTvProgram, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist, BaseItemKind.MusicVideo, BaseItemKind.Series, BaseItemKind.Trailer ] }, { BaseItemKind.MusicArtist, [ BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicVideo ] }, { BaseItemKind.MusicGenre, [ BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist, BaseItemKind.MusicVideo ] }, { BaseItemKind.Person, [ BaseItemKind.Audio, BaseItemKind.AudioBook, BaseItemKind.Book, BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.LiveTvProgram, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist, BaseItemKind.MusicVideo, BaseItemKind.Series, BaseItemKind.Trailer ] }, { BaseItemKind.Studio, [ BaseItemKind.Audio, BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.LiveTvProgram, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist, BaseItemKind.MusicVideo, BaseItemKind.Series, BaseItemKind.Trailer ] }, { BaseItemKind.Year, [ BaseItemKind.Audio, BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.LiveTvProgram, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist, BaseItemKind.MusicVideo, BaseItemKind.Series, BaseItemKind.Trailer ] } }.ToFrozenDictionary(); private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; private readonly IImageProcessor _imageProcessor; private readonly IProviderManager _providerManager; private readonly IRecordingsManager _recordingsManager; private readonly IApplicationHost _appHost; private readonly IMediaSourceManager _mediaSourceManager; private readonly Lazy _livetvManagerFactory; private readonly ITrickplayManager _trickplayManager; private readonly IChapterManager _chapterManager; public DtoService( ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IImageProcessor imageProcessor, IProviderManager providerManager, IRecordingsManager recordingsManager, IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy livetvManagerFactory, ITrickplayManager trickplayManager, IChapterManager chapterManager) { _logger = logger; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _imageProcessor = imageProcessor; _providerManager = providerManager; _recordingsManager = recordingsManager; _appHost = appHost; _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _trickplayManager = trickplayManager; _chapterManager = chapterManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; /// public IReadOnlyList GetBaseItemDtos( IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null, bool skipVisibilityCheck = false) { var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToList(); var returnItems = new BaseItemDto[accessibleItems.Count]; List<(BaseItem, BaseItemDto)>? programTuples = null; List<(BaseItemDto, LiveTvChannel)>? channelTuples = null; // Batch-fetch user data for all items Dictionary? userDataBatch = null; IReadOnlyDictionary? resumeDataBatch = null; if (user is not null && options.EnableUserData) { userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user); // For items with alternate versions, the most recently played version drives resume. resumeDataBatch = _userDataRepository.GetResumeUserDataBatch(accessibleItems, user); } // Pre-compute collection folders once to avoid N+1 queries in CanDelete List? allCollectionFolders = null; if (user is not null && options.ContainsField(ItemFields.CanDelete)) { allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType().ToList(); } // Batch-fetch child counts for all folders to avoid N+1 queries Dictionary? childCountBatch = null; if (options.ContainsField(ItemFields.ChildCount)) { var folderIds = accessibleItems.OfType().Select(f => f.Id).ToList(); if (folderIds.Count > 0) { childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id); } } // Batch-fetch played/total counts for all folders to avoid N+1 queries Dictionary? playedCountBatch = null; if (user is not null && options.EnableUserData) { var folderIds = accessibleItems.OfType() .Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemFields.RecursiveItemCount))) .Select(f => f.Id).ToList(); if (folderIds.Count > 0) { playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user); } } // Batch-fetch MusicArtist lookups across all items to avoid N+1 queries. IReadOnlyDictionary? artistsBatch = null; var artistNames = new HashSet(StringComparer.Ordinal); foreach (var item in accessibleItems) { if (item is IHasArtist hasArtist) { foreach (var name in hasArtist.Artists) { if (!string.IsNullOrWhiteSpace(name)) { artistNames.Add(name); } } } if (item is IHasAlbumArtist hasAlbumArtist) { foreach (var name in hasAlbumArtist.AlbumArtists) { if (!string.IsNullOrWhiteSpace(name)) { artistNames.Add(name); } } } } if (artistNames.Count > 0) { artistsBatch = _libraryManager.GetArtists(artistNames.ToArray()); } // Batch-fetch people across all items to avoid one GetPeople query per item. IReadOnlyDictionary>? peopleBatch = null; if (options.ContainsField(ItemFields.People)) { var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList(); if (peopleItemIds.Count > 0) { peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds); } } // Batch-detect which videos own alternate versions to avoid the per-item alternate-version // queries in MediaSourceCount. Videos absent from this set have a single media source. IReadOnlySet? alternateVersionItemIds = null; if (options.ContainsField(ItemFields.MediaSourceCount)) { var versionItemIds = accessibleItems.OfType