< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Dto.DtoService
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Dto/DtoService.cs
Line coverage
53%
Covered lines: 411
Uncovered lines: 358
Coverable lines: 769
Total lines: 1650
Line coverage: 53.4%
Branch coverage
38%
Covered branches: 199
Total branches: 522
Branch coverage: 38.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 48.5% (353/727) Branch coverage: 35% (166/473) Total lines: 15261/29/2026 - 12:13:32 AM Line coverage: 49.7% (353/710) Branch coverage: 35% (166/473) Total lines: 15114/6/2026 - 12:13:55 AM Line coverage: 49.4% (353/714) Branch coverage: 34.8% (167/479) Total lines: 15205/4/2026 - 12:15:16 AM Line coverage: 53.6% (404/753) Branch coverage: 38.6% (194/502) Total lines: 16125/6/2026 - 12:15:23 AM Line coverage: 53.4% (411/769) Branch coverage: 38.1% (199/522) Total lines: 1650 1/23/2026 - 12:11:06 AM Line coverage: 48.5% (353/727) Branch coverage: 35% (166/473) Total lines: 15261/29/2026 - 12:13:32 AM Line coverage: 49.7% (353/710) Branch coverage: 35% (166/473) Total lines: 15114/6/2026 - 12:13:55 AM Line coverage: 49.4% (353/714) Branch coverage: 34.8% (167/479) Total lines: 15205/4/2026 - 12:15:16 AM Line coverage: 53.6% (404/753) Branch coverage: 38.6% (194/502) Total lines: 16125/6/2026 - 12:15:23 AM Line coverage: 53.4% (411/769) Branch coverage: 38.1% (199/522) Total lines: 1650

Coverage delta

Coverage delta 5 -5

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Dto/DtoService.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Frozen;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.IO;
 8using System.Linq;
 9using Jellyfin.Data.Enums;
 10using Jellyfin.Database.Implementations.Entities;
 11using Jellyfin.Extensions;
 12using MediaBrowser.Common;
 13using MediaBrowser.Controller.Channels;
 14using MediaBrowser.Controller.Chapters;
 15using MediaBrowser.Controller.Drawing;
 16using MediaBrowser.Controller.Dto;
 17using MediaBrowser.Controller.Entities;
 18using MediaBrowser.Controller.Entities.Audio;
 19using MediaBrowser.Controller.Library;
 20using MediaBrowser.Controller.LiveTv;
 21using MediaBrowser.Controller.Playlists;
 22using MediaBrowser.Controller.Providers;
 23using MediaBrowser.Controller.Trickplay;
 24using MediaBrowser.Model.Dto;
 25using MediaBrowser.Model.Entities;
 26using MediaBrowser.Model.Querying;
 27using Microsoft.Extensions.Logging;
 28using Book = MediaBrowser.Controller.Entities.Book;
 29using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 30using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 31using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 32using Person = MediaBrowser.Controller.Entities.Person;
 33using Photo = MediaBrowser.Controller.Entities.Photo;
 34using Season = MediaBrowser.Controller.Entities.TV.Season;
 35using Series = MediaBrowser.Controller.Entities.TV.Series;
 36
 37namespace Emby.Server.Implementations.Dto
 38{
 39    public class DtoService : IDtoService
 40    {
 141        private static readonly FrozenDictionary<BaseItemKind, BaseItemKind[]> _relatedItemKinds = new Dictionary<BaseIt
 142        {
 143            {
 144                BaseItemKind.Genre, [
 145                    BaseItemKind.Audio,
 146                    BaseItemKind.Episode,
 147                    BaseItemKind.Movie,
 148                    BaseItemKind.LiveTvProgram,
 149                    BaseItemKind.MusicAlbum,
 150                    BaseItemKind.MusicArtist,
 151                    BaseItemKind.MusicVideo,
 152                    BaseItemKind.Series,
 153                    BaseItemKind.Trailer
 154                ]
 155            },
 156            {
 157                BaseItemKind.MusicArtist, [
 158                    BaseItemKind.Audio,
 159                    BaseItemKind.MusicAlbum,
 160                    BaseItemKind.MusicVideo
 161                ]
 162            },
 163            {
 164                BaseItemKind.MusicGenre, [
 165                    BaseItemKind.Audio,
 166                    BaseItemKind.MusicAlbum,
 167                    BaseItemKind.MusicArtist,
 168                    BaseItemKind.MusicVideo
 169                ]
 170            },
 171            {
 172                BaseItemKind.Person, [
 173                    BaseItemKind.Audio,
 174                    BaseItemKind.Episode,
 175                    BaseItemKind.Movie,
 176                    BaseItemKind.LiveTvProgram,
 177                    BaseItemKind.MusicAlbum,
 178                    BaseItemKind.MusicArtist,
 179                    BaseItemKind.MusicVideo,
 180                    BaseItemKind.Series,
 181                    BaseItemKind.Trailer
 182                ]
 183            },
 184            {
 185                BaseItemKind.Studio, [
 186                    BaseItemKind.Audio,
 187                    BaseItemKind.Episode,
 188                    BaseItemKind.Movie,
 189                    BaseItemKind.LiveTvProgram,
 190                    BaseItemKind.MusicAlbum,
 191                    BaseItemKind.MusicArtist,
 192                    BaseItemKind.MusicVideo,
 193                    BaseItemKind.Series,
 194                    BaseItemKind.Trailer
 195                ]
 196            },
 197            {
 198                BaseItemKind.Year, [
 199                    BaseItemKind.Audio,
 1100                    BaseItemKind.Episode,
 1101                    BaseItemKind.Movie,
 1102                    BaseItemKind.LiveTvProgram,
 1103                    BaseItemKind.MusicAlbum,
 1104                    BaseItemKind.MusicArtist,
 1105                    BaseItemKind.MusicVideo,
 1106                    BaseItemKind.Series,
 1107                    BaseItemKind.Trailer
 1108                ]
 1109            }
 1110        }.ToFrozenDictionary();
 111
 112        private readonly ILogger<DtoService> _logger;
 113        private readonly ILibraryManager _libraryManager;
 114        private readonly IUserDataManager _userDataRepository;
 115
 116        private readonly IImageProcessor _imageProcessor;
 117        private readonly IProviderManager _providerManager;
 118        private readonly IRecordingsManager _recordingsManager;
 119
 120        private readonly IApplicationHost _appHost;
 121        private readonly IMediaSourceManager _mediaSourceManager;
 122        private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 123
 124        private readonly ITrickplayManager _trickplayManager;
 125        private readonly IChapterManager _chapterManager;
 126
 127        public DtoService(
 128            ILogger<DtoService> logger,
 129            ILibraryManager libraryManager,
 130            IUserDataManager userDataRepository,
 131            IImageProcessor imageProcessor,
 132            IProviderManager providerManager,
 133            IRecordingsManager recordingsManager,
 134            IApplicationHost appHost,
 135            IMediaSourceManager mediaSourceManager,
 136            Lazy<ILiveTvManager> livetvManagerFactory,
 137            ITrickplayManager trickplayManager,
 138            IChapterManager chapterManager)
 139        {
 21140            _logger = logger;
 21141            _libraryManager = libraryManager;
 21142            _userDataRepository = userDataRepository;
 21143            _imageProcessor = imageProcessor;
 21144            _providerManager = providerManager;
 21145            _recordingsManager = recordingsManager;
 21146            _appHost = appHost;
 21147            _mediaSourceManager = mediaSourceManager;
 21148            _livetvManagerFactory = livetvManagerFactory;
 21149            _trickplayManager = trickplayManager;
 21150            _chapterManager = chapterManager;
 21151        }
 152
 0153        private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
 154
 155        /// <inheritdoc />
 156        public IReadOnlyList<BaseItemDto> GetBaseItemDtos(
 157            IReadOnlyList<BaseItem> items,
 158            DtoOptions options,
 159            User? user = null,
 160            BaseItem? owner = null,
 161            bool skipVisibilityCheck = false)
 162        {
 4163            var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToLi
 4164            var returnItems = new BaseItemDto[accessibleItems.Count];
 4165            List<(BaseItem, BaseItemDto)>? programTuples = null;
 4166            List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
 167
 168            // Batch-fetch user data for all items
 4169            Dictionary<Guid, UserItemData>? userDataBatch = null;
 4170            if (user is not null && options.EnableUserData)
 171            {
 4172                userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
 173            }
 174
 175            // Pre-compute collection folders once to avoid N+1 queries in CanDelete
 4176            List<Folder>? allCollectionFolders = null;
 4177            if (user is not null && options.ContainsField(ItemFields.CanDelete))
 178            {
 0179                allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 180            }
 181
 182            // Batch-fetch child counts for all folders to avoid N+1 queries
 4183            Dictionary<Guid, int>? childCountBatch = null;
 4184            if (options.ContainsField(ItemFields.ChildCount))
 185            {
 0186                var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
 0187                if (folderIds.Count > 0)
 188                {
 0189                    childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
 190                }
 191            }
 192
 193            // Batch-fetch played/total counts for all folders to avoid N+1 queries
 4194            Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null;
 4195            if (user is not null && options.EnableUserData)
 196            {
 4197                var folderIds = accessibleItems.OfType<Folder>()
 4198                    .Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemF
 4199                    .Select(f => f.Id).ToList();
 4200                if (folderIds.Count > 0)
 201                {
 0202                    playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
 203                }
 204            }
 205
 206            // Batch-fetch MusicArtist lookups across all items to avoid N+1 queries.
 4207            IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null;
 4208            var artistNames = new HashSet<string>(StringComparer.Ordinal);
 14209            foreach (var item in accessibleItems)
 210            {
 3211                if (item is IHasArtist hasArtist)
 212                {
 0213                    foreach (var name in hasArtist.Artists)
 214                    {
 0215                        if (!string.IsNullOrWhiteSpace(name))
 216                        {
 0217                            artistNames.Add(name);
 218                        }
 219                    }
 220                }
 221
 3222                if (item is IHasAlbumArtist hasAlbumArtist)
 223                {
 0224                    foreach (var name in hasAlbumArtist.AlbumArtists)
 225                    {
 0226                        if (!string.IsNullOrWhiteSpace(name))
 227                        {
 0228                            artistNames.Add(name);
 229                        }
 230                    }
 231                }
 232            }
 233
 4234            if (artistNames.Count > 0)
 235            {
 0236                artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
 237            }
 238
 14239            for (int index = 0; index < accessibleItems.Count; index++)
 240            {
 3241                var item = accessibleItems[index];
 3242                var dto = GetBaseItemDtoInternal(
 3243                    item,
 3244                    options,
 3245                    user,
 3246                    owner,
 3247                    userDataBatch?.GetValueOrDefault(item.Id),
 3248                    allCollectionFolders,
 3249                    childCountBatch,
 3250                    playedCountBatch,
 3251                    artistsBatch);
 252
 3253                if (item is LiveTvChannel tvChannel)
 254                {
 0255                    (channelTuples ??= []).Add((dto, tvChannel));
 256                }
 3257                else if (item is LiveTvProgram)
 258                {
 0259                    (programTuples ??= []).Add((item, dto));
 260                }
 261
 3262                if (options.ContainsField(ItemFields.ItemCounts))
 263                {
 0264                    SetItemByNameInfo(dto, user);
 265                }
 266
 3267                returnItems[index] = dto;
 268            }
 269
 4270            if (programTuples is not null)
 271            {
 0272                LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
 273            }
 274
 4275            if (channelTuples is not null)
 276            {
 0277                LivetvManager.AddChannelInfo(channelTuples, options, user);
 278            }
 279
 4280            return returnItems;
 281        }
 282
 283        public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
 284        {
 6285            var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
 6286            if (item is LiveTvChannel tvChannel)
 287            {
 0288                LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
 289            }
 6290            else if (item is LiveTvProgram)
 291            {
 0292                LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
 293            }
 294
 6295            if (options.ContainsField(ItemFields.ItemCounts))
 296            {
 6297                SetItemByNameInfo(dto, user);
 298            }
 299
 6300            return dto;
 301        }
 302
 303        private BaseItemDto GetBaseItemDtoInternal(
 304            BaseItem item,
 305            DtoOptions options,
 306            User? user = null,
 307            BaseItem? owner = null,
 308            UserItemData? userData = null,
 309            List<Folder>? allCollectionFolders = null,
 310            Dictionary<Guid, int>? childCountBatch = null,
 311            Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
 312            IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
 313        {
 9314            var dto = new BaseItemDto
 9315            {
 9316                ServerId = _appHost.SystemId
 9317            };
 318
 9319            if (item.SourceType == SourceType.Channel)
 320            {
 0321                dto.SourceType = item.SourceType.ToString();
 322            }
 323
 9324            if (options.ContainsField(ItemFields.People))
 325            {
 6326                AttachPeople(dto, item, user);
 327            }
 328
 9329            if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
 330            {
 331                try
 332                {
 6333                    AttachPrimaryImageAspectRatio(dto, item);
 6334                }
 0335                catch (Exception ex)
 336                {
 337                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 0338                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
 0339                }
 340            }
 341
 9342            if (options.ContainsField(ItemFields.DisplayPreferencesId))
 343            {
 6344                dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture);
 345            }
 346
 9347            if (user is not null)
 348            {
 9349                AttachUserSpecificInfo(
 9350                    dto,
 9351                    item,
 9352                    user,
 9353                    options,
 9354                    userData,
 9355                    childCountBatch,
 9356                    playedCountBatch);
 357            }
 358
 9359            if (item is IHasMediaSources
 9360                && options.ContainsField(ItemFields.MediaSources))
 361            {
 0362                dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray();
 363
 0364                NormalizeMediaSourceContainers(dto);
 365            }
 366
 9367            if (options.ContainsField(ItemFields.Studios))
 368            {
 6369                AttachStudios(dto, item);
 370            }
 371
 9372            AttachBasicFields(dto, item, owner, options, artistsBatch);
 373
 9374            if (options.ContainsField(ItemFields.CanDelete))
 375            {
 6376                dto.CanDelete = user is null
 6377                    ? item.CanDelete()
 6378                    : allCollectionFolders is not null
 6379                        ? item.CanDelete(user, allCollectionFolders)
 6380                        : item.CanDelete(user);
 381            }
 382
 9383            if (options.ContainsField(ItemFields.CanDownload))
 384            {
 6385                dto.CanDownload = user is null
 6386                    ? item.CanDownload()
 6387                    : item.CanDownload(user);
 388            }
 389
 9390            if (options.ContainsField(ItemFields.Etag))
 391            {
 6392                dto.Etag = item.GetEtag(user);
 393            }
 394
 9395            var activeRecording = _recordingsManager.GetActiveRecordingInfo(item.Path);
 9396            if (activeRecording is not null)
 397            {
 0398                dto.Type = BaseItemKind.Recording;
 0399                dto.CanDownload = false;
 0400                dto.RunTimeTicks = null;
 401
 0402                if (!string.IsNullOrEmpty(dto.SeriesName))
 403                {
 0404                    dto.EpisodeTitle = dto.Name;
 0405                    dto.Name = dto.SeriesName;
 406                }
 407
 0408                LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
 409            }
 410
 9411            if (item is Audio audio)
 412            {
 0413                dto.HasLyrics = audio.GetMediaStreams().Any(s => s.Type == MediaStreamType.Lyric);
 414            }
 415
 9416            return dto;
 417        }
 418
 419        private static void NormalizeMediaSourceContainers(BaseItemDto dto)
 420        {
 0421            foreach (var mediaSource in dto.MediaSources)
 422            {
 0423                var container = mediaSource.Container;
 0424                if (string.IsNullOrEmpty(container))
 425                {
 426                    continue;
 427                }
 428
 0429                var containers = container.Split(',');
 0430                if (containers.Length < 2)
 431                {
 432                    continue;
 433                }
 434
 0435                var path = mediaSource.Path;
 0436                string? fileExtensionContainer = null;
 437
 0438                if (!string.IsNullOrEmpty(path))
 439                {
 0440                    path = Path.GetExtension(path);
 0441                    if (!string.IsNullOrEmpty(path))
 442                    {
 0443                        path = Path.GetExtension(path);
 0444                        if (!string.IsNullOrEmpty(path))
 445                        {
 0446                            path = path.TrimStart('.');
 447                        }
 448
 0449                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase)
 450                        {
 0451                            fileExtensionContainer = path;
 452                        }
 453                    }
 454                }
 455
 0456                mediaSource.Container = fileExtensionContainer ?? containers[0];
 457            }
 0458        }
 459
 460        /// <inheritdoc />
 461        /// TODO refactor this to use the new SetItemByNameInfo.
 462        /// Some callers already have the counts extracted so no reason to retrieve them again.
 463        public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user =
 464        {
 0465            var dto = GetBaseItemDtoInternal(item, options, user);
 466
 0467            if (options.ContainsField(ItemFields.ItemCounts)
 0468                && taggedItems is not null
 0469                && taggedItems.Count != 0)
 470            {
 0471                SetItemByNameInfo(item, dto, taggedItems);
 472            }
 473
 0474            return dto;
 475        }
 476
 477        private void SetItemByNameInfo(BaseItemDto dto, User? user)
 478        {
 6479            if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
 480            {
 6481                return;
 482            }
 483
 0484            var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
 485
 0486            dto.AlbumCount = counts.AlbumCount;
 0487            dto.ArtistCount = counts.ArtistCount;
 0488            dto.EpisodeCount = counts.EpisodeCount;
 0489            dto.MovieCount = counts.MovieCount;
 0490            dto.MusicVideoCount = counts.MusicVideoCount;
 0491            dto.ProgramCount = counts.ProgramCount;
 0492            dto.SeriesCount = counts.SeriesCount;
 0493            dto.SongCount = counts.SongCount;
 0494            dto.TrailerCount = counts.TrailerCount;
 0495            dto.ChildCount = counts.TotalItemCount();
 0496        }
 497
 498        private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
 499        {
 0500            if (item is MusicArtist)
 501            {
 0502                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0503                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0504                dto.SongCount = taggedItems.Count(i => i is Audio);
 505            }
 0506            else if (item is MusicGenre)
 507            {
 0508                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
 0509                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0510                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0511                dto.SongCount = taggedItems.Count(i => i is Audio);
 512            }
 513            else
 514            {
 515                // This populates them all and covers Genre, Person, Studio, Year
 516
 0517                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
 0518                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0519                dto.EpisodeCount = taggedItems.Count(i => i is Episode);
 0520                dto.MovieCount = taggedItems.Count(i => i is Movie);
 0521                dto.TrailerCount = taggedItems.Count(i => i is Trailer);
 0522                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0523                dto.SeriesCount = taggedItems.Count(i => i is Series);
 0524                dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram);
 0525                dto.SongCount = taggedItems.Count(i => i is Audio);
 526            }
 527
 0528            dto.ChildCount = taggedItems.Count;
 0529        }
 530
 531        /// <summary>
 532        /// Attaches the user specific info.
 533        /// </summary>
 534        private void AttachUserSpecificInfo(
 535            BaseItemDto dto,
 536            BaseItem item,
 537            User user,
 538            DtoOptions options,
 539            UserItemData? userData = null,
 540            Dictionary<Guid, int>? childCountBatch = null,
 541            Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
 542        {
 9543            if (item.IsFolder)
 544            {
 9545                var folder = (Folder)item;
 546
 9547                if (options.EnableUserData)
 548                {
 9549                    if (userData is not null)
 550                    {
 551                        // Use pre-fetched user data
 3552                        dto.UserData = GetUserItemDataDto(userData, item.Id);
 3553                        (int Played, int Total)? precomputed = playedCountBatch is not null
 3554                            && playedCountBatch.TryGetValue(item.Id, out var counts) ? counts : null;
 3555                        item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options, precomputed);
 556                    }
 557                    else
 558                    {
 559                        // Fall back to individual fetch
 6560                        dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
 561                    }
 562                }
 563
 9564                if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
 565                {
 566                    // For these types we can try to optimize and assume these values will be equal
 9567                    if (item is MusicAlbum || item is Season || item is Playlist)
 568                    {
 0569                        dto.ChildCount = dto.RecursiveItemCount;
 0570                        var folderChildCount = folder.LinkedChildren.Length;
 571                        // The default is an empty array, so we can't reliably use the count when it's empty
 0572                        if (folderChildCount > 0)
 573                        {
 0574                            dto.ChildCount ??= folderChildCount;
 575                        }
 576                    }
 577
 9578                    if (options.ContainsField(ItemFields.ChildCount))
 579                    {
 6580                        dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
 581                    }
 582                }
 583
 9584                if (options.ContainsField(ItemFields.CumulativeRunTimeTicks))
 585                {
 6586                    dto.CumulativeRunTimeTicks = item.RunTimeTicks;
 587                }
 588
 9589                if (options.ContainsField(ItemFields.DateLastMediaAdded))
 590                {
 6591                    dto.DateLastMediaAdded = folder.DateLastMediaAdded;
 592                }
 593            }
 594            else
 595            {
 0596                if (options.EnableUserData)
 597                {
 0598                    if (userData is not null)
 599                    {
 600                        // Use pre-fetched user data
 0601                        dto.UserData = GetUserItemDataDto(userData, item.Id);
 0602                        item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
 603                    }
 604                    else
 605                    {
 606                        // Fall back to individual fetch
 0607                        dto.UserData = _userDataRepository.GetUserDataDto(item, user);
 608                    }
 609                }
 610            }
 611
 9612            if (options.ContainsField(ItemFields.PlayAccess))
 613            {
 6614                dto.PlayAccess = item.GetPlayAccess(user);
 615            }
 9616        }
 617
 618        private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
 619        {
 3620            ArgumentNullException.ThrowIfNull(data);
 621
 3622            return new UserItemDataDto
 3623            {
 3624                IsFavorite = data.IsFavorite,
 3625                Likes = data.Likes,
 3626                PlaybackPositionTicks = data.PlaybackPositionTicks,
 3627                PlayCount = data.PlayCount,
 3628                Rating = data.Rating,
 3629                Played = data.Played,
 3630                LastPlayedDate = data.LastPlayedDate,
 3631                ItemId = itemId,
 3632                Key = data.Key
 3633            };
 634        }
 635
 636        private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
 637        {
 638            // Right now this is too slow to calculate for top level folders on a per-user basis
 639            // Just return something so that apps that are expecting a value won't think the folders are empty
 6640            if (folder is ICollectionFolder || folder is UserView)
 641            {
 0642                return Random.Shared.Next(1, 10);
 643            }
 644
 645            // Use pre-fetched batch data if available
 6646            if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
 647            {
 0648                return count;
 649            }
 650
 651            // Fall back to individual query for special cases (Series, Season, etc.)
 6652            return folder.GetChildCount(user);
 653        }
 654
 655        private static void SetBookProperties(BaseItemDto dto, Book item)
 656        {
 0657            dto.SeriesName = item.SeriesName;
 0658        }
 659
 660        private static void SetPhotoProperties(BaseItemDto dto, Photo item)
 661        {
 0662            dto.CameraMake = item.CameraMake;
 0663            dto.CameraModel = item.CameraModel;
 0664            dto.Software = item.Software;
 0665            dto.ExposureTime = item.ExposureTime;
 0666            dto.FocalLength = item.FocalLength;
 0667            dto.ImageOrientation = item.Orientation;
 0668            dto.Aperture = item.Aperture;
 0669            dto.ShutterSpeed = item.ShutterSpeed;
 670
 0671            dto.Latitude = item.Latitude;
 0672            dto.Longitude = item.Longitude;
 0673            dto.Altitude = item.Altitude;
 0674            dto.IsoSpeedRating = item.IsoSpeedRating;
 675
 0676            var album = item.AlbumEntity;
 677
 0678            if (album is not null)
 679            {
 0680                dto.Album = album.Name;
 0681                dto.AlbumId = album.Id;
 682            }
 0683        }
 684
 685        private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
 686        {
 0687            if (!string.IsNullOrEmpty(item.Album))
 688            {
 0689                var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
 0690                {
 0691                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
 0692                    Name = item.Album,
 0693                    Limit = 1
 0694                });
 695
 0696                if (parentAlbumIds.Count > 0)
 697                {
 0698                    dto.AlbumId = parentAlbumIds[0];
 699                }
 700            }
 701
 0702            dto.Album = item.Album;
 0703        }
 704
 705        private string[] GetImageTags(BaseItem item, List<ItemImageInfo> images)
 706        {
 9707            return images
 9708                .Select(p => GetImageCacheTag(item, p))
 9709                .Where(i => i is not null)
 9710                .ToArray()!; // null values got filtered out
 711        }
 712
 713        private string? GetImageCacheTag(BaseItem item, ItemImageInfo image)
 714        {
 715            try
 716            {
 0717                return _imageProcessor.GetImageCacheTag(item, image);
 718            }
 0719            catch (Exception ex)
 720            {
 0721                _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
 0722                return null;
 723            }
 0724        }
 725
 726        /// <summary>
 727        /// Attaches People DTO's to a DTOBaseItem.
 728        /// </summary>
 729        /// <param name="dto">The dto.</param>
 730        /// <param name="item">The item.</param>
 731        /// <param name="user">The requesting user.</param>
 732        private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
 733        {
 734            // Ordering by person type to ensure actors and artists are at the front.
 735            // This is taking advantage of the fact that they both begin with A
 736            // This should be improved in the future
 6737            var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
 6738                .ThenBy(i =>
 6739                {
 6740                    if (i.IsType(PersonKind.Actor))
 6741                    {
 6742                        return 0;
 6743                    }
 6744
 6745                    if (i.IsType(PersonKind.GuestStar))
 6746                    {
 6747                        return 1;
 6748                    }
 6749
 6750                    if (i.IsType(PersonKind.Director))
 6751                    {
 6752                        return 2;
 6753                    }
 6754
 6755                    if (i.IsType(PersonKind.Writer))
 6756                    {
 6757                        return 3;
 6758                    }
 6759
 6760                    if (i.IsType(PersonKind.Producer))
 6761                    {
 6762                        return 4;
 6763                    }
 6764
 6765                    if (i.IsType(PersonKind.Composer))
 6766                    {
 6767                        return 4;
 6768                    }
 6769
 6770                    return 10;
 6771                })
 6772                .ToList();
 773
 6774            var list = new List<BaseItemPerson>();
 775
 6776            Dictionary<string, Person> dictionary = people.Select(p => p.Name)
 6777                .Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
 6778                {
 6779                    try
 6780                    {
 6781                        return _libraryManager.GetPerson(c);
 6782                    }
 6783                    catch (Exception ex)
 6784                    {
 6785                        _logger.LogError(ex, "Error getting person {Name}", c);
 6786                        return null;
 6787                    }
 6788                }).Where(i => i is not null)
 6789                .Where(i => user is null || i!.IsVisible(user))
 6790                .DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase)
 6791                .ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out
 792
 12793            for (var i = 0; i < people.Count; i++)
 794            {
 0795                var person = people[i];
 796
 0797                var baseItemPerson = new BaseItemPerson
 0798                {
 0799                    Name = person.Name,
 0800                    Role = person.Role,
 0801                    Type = person.Type
 0802                };
 803
 0804                if (dictionary.TryGetValue(person.Name, out Person? entity))
 805                {
 0806                    baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
 0807                    baseItemPerson.Id = entity.Id;
 0808                    if (dto.ImageBlurHashes is not null)
 809                    {
 810                        // Only add BlurHash for the person's image.
 0811                        baseItemPerson.ImageBlurHashes = [];
 0812                        foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
 813                        {
 0814                            if (blurHash is not null)
 815                            {
 0816                                baseItemPerson.ImageBlurHashes[imageType] = [];
 0817                                foreach (var (imageId, blurHashValue) in blurHash)
 818                                {
 0819                                    if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalI
 820                                    {
 0821                                        baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
 822                                    }
 823                                }
 824                            }
 825                        }
 826                    }
 827
 0828                    list.Add(baseItemPerson);
 829                }
 830            }
 831
 6832            dto.People = list.ToArray();
 6833        }
 834
 835        /// <summary>
 836        /// Attaches the studios.
 837        /// </summary>
 838        /// <param name="dto">The dto.</param>
 839        /// <param name="item">The item.</param>
 840        private void AttachStudios(BaseItemDto dto, BaseItem item)
 841        {
 6842            dto.Studios = item.Studios
 6843                .Where(i => !string.IsNullOrEmpty(i))
 6844                .Select(i => new NameGuidPair
 6845                {
 6846                    Name = i,
 6847                    Id = _libraryManager.GetStudioId(i)
 6848                })
 6849                .ToArray();
 6850        }
 851
 852        private void AttachGenreItems(BaseItemDto dto, BaseItem item)
 853        {
 6854            dto.GenreItems = item.Genres
 6855                .Where(i => !string.IsNullOrEmpty(i))
 6856                .Select(i => new NameGuidPair
 6857                {
 6858                    Name = i,
 6859                    Id = GetGenreId(i, item)
 6860                })
 6861                .ToArray();
 6862        }
 863
 864        private Guid GetGenreId(string name, BaseItem owner)
 865        {
 0866            if (owner is IHasMusicGenres)
 867            {
 0868                return _libraryManager.GetMusicGenreId(name);
 869            }
 870
 0871            return _libraryManager.GetGenreId(name);
 872        }
 873
 874        private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
 875        {
 0876            var image = item.GetImageInfo(imageType, imageIndex);
 0877            if (image is not null)
 878            {
 0879                return GetTagAndFillBlurhash(dto, item, image);
 880            }
 881
 0882            return null;
 883        }
 884
 885        private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
 886        {
 0887            var tag = GetImageCacheTag(item, image);
 0888            if (tag is null)
 889            {
 0890                return null;
 891            }
 892
 0893            if (!string.IsNullOrEmpty(image.BlurHash))
 894            {
 0895                dto.ImageBlurHashes ??= [];
 896
 0897                if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
 898                {
 0899                    value = [];
 0900                    dto.ImageBlurHashes[image.Type] = value;
 901                }
 902
 0903                value[tag] = image.BlurHash;
 904            }
 905
 0906            return tag;
 907        }
 908
 909        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit)
 910        {
 9911            return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList());
 912        }
 913
 914        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInf
 915        {
 9916            var tags = GetImageTags(item, images);
 9917            var hashes = new Dictionary<string, string>();
 18918            for (int i = 0; i < images.Count; i++)
 919            {
 0920                var img = images[i];
 0921                if (!string.IsNullOrEmpty(img.BlurHash))
 922                {
 0923                    var tag = tags[i];
 0924                    hashes[tag] = img.BlurHash;
 925                }
 926            }
 927
 9928            if (hashes.Count > 0)
 929            {
 0930                dto.ImageBlurHashes ??= [];
 931
 0932                dto.ImageBlurHashes[imageType] = hashes;
 933            }
 934
 9935            return tags;
 936        }
 937
 938        /// <summary>
 939        /// Sets simple property values on a DTOBaseItem.
 940        /// </summary>
 941        /// <param name="dto">The dto.</param>
 942        /// <param name="item">The item.</param>
 943        /// <param name="owner">The owner.</param>
 944        /// <param name="options">The options.</param>
 945        /// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
 946        private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDic
 947        {
 9948            if (options.ContainsField(ItemFields.DateCreated))
 949            {
 6950                dto.DateCreated = item.DateCreated;
 951            }
 952
 9953            if (options.ContainsField(ItemFields.Settings))
 954            {
 6955                dto.LockedFields = item.LockedFields;
 6956                dto.LockData = item.IsLocked;
 6957                dto.ForcedSortName = item.ForcedSortName;
 958            }
 959
 9960            dto.Container = item.Container;
 9961            dto.EndDate = item.EndDate;
 962
 9963            if (options.ContainsField(ItemFields.ExternalUrls))
 964            {
 6965                dto.ExternalUrls = _providerManager.GetExternalUrls(item).ToArray();
 966            }
 967
 9968            if (options.ContainsField(ItemFields.Tags))
 969            {
 6970                dto.Tags = item.Tags;
 971            }
 972
 9973            if (item is IHasAspectRatio hasAspectRatio)
 974            {
 0975                dto.AspectRatio = hasAspectRatio.AspectRatio;
 976            }
 977
 9978            dto.ImageBlurHashes = [];
 979
 9980            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
 9981            if (backdropLimit > 0)
 982            {
 9983                dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
 984            }
 985
 9986            if (options.ContainsField(ItemFields.Genres))
 987            {
 6988                dto.Genres = item.Genres;
 6989                AttachGenreItems(dto, item);
 990            }
 991
 9992            if (options.EnableImages)
 993            {
 9994                dto.ImageTags = [];
 995
 996                // Prevent implicitly captured closure
 9997                var currentItem = item;
 18998                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)))
 999                {
 01000                    if (options.GetImageLimit(image.Type) > 0)
 1001                    {
 01002                        var tag = GetTagAndFillBlurhash(dto, item, image);
 1003
 01004                        if (tag is not null)
 1005                        {
 01006                            dto.ImageTags[image.Type] = tag;
 1007                        }
 1008                    }
 1009                }
 1010            }
 1011
 91012            dto.Id = item.Id;
 91013            dto.IndexNumber = item.IndexNumber;
 91014            dto.ParentIndexNumber = item.ParentIndexNumber;
 1015
 91016            if (item.IsFolder)
 1017            {
 91018                dto.IsFolder = true;
 1019            }
 01020            else if (item is IHasMediaSources)
 1021            {
 01022                dto.IsFolder = false;
 1023            }
 1024
 91025            dto.MediaType = item.MediaType;
 1026
 91027            if (item is not LiveTvProgram)
 1028            {
 91029                dto.LocationType = item.LocationType;
 1030            }
 1031
 91032            dto.Audio = item.Audio;
 1033
 91034            if (options.ContainsField(ItemFields.Settings))
 1035            {
 61036                dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 61037                dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 1038            }
 1039
 91040            dto.CriticRating = item.CriticRating;
 1041
 91042            if (item is IHasDisplayOrder hasDisplayOrder)
 1043            {
 01044                dto.DisplayOrder = hasDisplayOrder.DisplayOrder;
 1045            }
 1046
 91047            if (item is IHasCollectionType hasCollectionType)
 1048            {
 31049                dto.CollectionType = hasCollectionType.CollectionType;
 1050            }
 1051
 91052            if (options.ContainsField(ItemFields.RemoteTrailers))
 1053            {
 61054                dto.RemoteTrailers = item.RemoteTrailers;
 1055            }
 1056
 91057            dto.Name = item.Name;
 91058            dto.OfficialRating = item.OfficialRating;
 1059
 91060            if (options.ContainsField(ItemFields.Overview))
 1061            {
 61062                dto.Overview = item.Overview;
 1063            }
 1064
 91065            if (options.ContainsField(ItemFields.OriginalTitle))
 1066            {
 61067                dto.OriginalTitle = item.OriginalTitle;
 1068            }
 1069
 91070            if (options.ContainsField(ItemFields.ParentId))
 1071            {
 61072                dto.ParentId = item.DisplayParentId;
 1073            }
 1074
 91075            AddInheritedImages(dto, item, options, owner);
 1076
 91077            if (options.ContainsField(ItemFields.Path))
 1078            {
 61079                dto.Path = GetMappedPath(item, owner);
 1080            }
 1081
 91082            if (options.ContainsField(ItemFields.EnableMediaSourceDisplay))
 1083            {
 61084                dto.EnableMediaSourceDisplay = item.EnableMediaSourceDisplay;
 1085            }
 1086
 91087            dto.PremiereDate = item.PremiereDate;
 91088            dto.ProductionYear = item.ProductionYear;
 1089
 91090            if (options.ContainsField(ItemFields.ProviderIds))
 1091            {
 61092                dto.ProviderIds = item.ProviderIds;
 1093            }
 1094
 91095            dto.RunTimeTicks = item.RunTimeTicks;
 1096
 91097            if (options.ContainsField(ItemFields.SortName))
 1098            {
 61099                dto.SortName = item.SortName;
 1100            }
 1101
 91102            if (options.ContainsField(ItemFields.CustomRating))
 1103            {
 61104                dto.CustomRating = item.CustomRating;
 1105            }
 1106
 91107            if (options.ContainsField(ItemFields.Taglines))
 1108            {
 61109                if (!string.IsNullOrEmpty(item.Tagline))
 1110                {
 01111                    dto.Taglines = new string[] { item.Tagline };
 1112                }
 1113
 61114                dto.Taglines ??= Array.Empty<string>();
 1115            }
 1116
 91117            dto.Type = item.GetBaseItemKind();
 91118            if ((item.CommunityRating ?? 0) > 0)
 1119            {
 01120                dto.CommunityRating = item.CommunityRating;
 1121            }
 1122
 91123            if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder)
 1124            {
 01125                dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
 1126            }
 1127
 91128            if (item.LUFS.HasValue)
 1129            {
 1130                // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
 01131                dto.NormalizationGain = -18f - item.LUFS;
 1132            }
 91133            else if (item.NormalizationGain.HasValue)
 1134            {
 01135                dto.NormalizationGain = item.NormalizationGain;
 1136            }
 1137
 1138            // Add audio info
 91139            if (item is Audio audio)
 1140            {
 01141                dto.Album = audio.Album;
 01142                dto.ExtraType = audio.ExtraType;
 1143
 01144                var albumParent = audio.AlbumEntity;
 1145
 01146                if (albumParent is not null)
 1147                {
 01148                    dto.AlbumId = albumParent.Id;
 01149                    dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
 01150                    if (albumParent.LUFS.HasValue)
 1151                    {
 1152                        // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
 01153                        dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
 1154                    }
 01155                    else if (albumParent.NormalizationGain.HasValue)
 1156                    {
 01157                        dto.AlbumNormalizationGain = albumParent.NormalizationGain;
 1158                    }
 1159                }
 1160
 1161                // if (options.ContainsField(ItemFields.MediaSourceCount))
 1162                // {
 1163                // Songs always have one
 1164                // }
 1165            }
 1166
 91167            if (item is IHasArtist hasArtist)
 1168            {
 01169                dto.Artists = hasArtist.Artists;
 1170
 1171                // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
 1172                // {
 1173                //    EnableTotalRecordCount = false,
 1174                //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
 1175                // });
 1176
 1177                // dto.ArtistItems = artistItems.Items
 1178                //    .Select(i =>
 1179                //    {
 1180                //        var artist = i.Item1;
 1181                //        return new NameIdPair
 1182                //        {
 1183                //            Name = artist.Name,
 1184                //            Id = artist.Id.ToString("N", CultureInfo.InvariantCulture)
 1185                //        };
 1186                //    })
 1187                //    .ToList();
 1188
 1189                // Include artists that are not in the database yet, e.g., just added via metadata editor
 1190                // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
 01191                var artistsLookup = artistsBatch
 01192                    ?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
 1193
 01194                dto.ArtistItems = hasArtist.Artists
 01195                    .Where(name => !string.IsNullOrWhiteSpace(name))
 01196                    .Distinct()
 01197                    .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
 01198                        ? new NameGuidPair { Name = name, Id = artists[0].Id }
 01199                        : null)
 01200                    .Where(item => item is not null)
 01201                    .ToArray();
 1202            }
 1203
 91204            if (item is IHasAlbumArtist hasAlbumArtist)
 1205            {
 01206                dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
 1207
 1208                // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
 1209                // {
 1210                //    EnableTotalRecordCount = false,
 1211                //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
 1212                // });
 1213
 1214                // dto.AlbumArtists = artistItems.Items
 1215                //    .Select(i =>
 1216                //    {
 1217                //        var artist = i.Item1;
 1218                //        return new NameIdPair
 1219                //        {
 1220                //            Name = artist.Name,
 1221                //            Id = artist.Id.ToString("N", CultureInfo.InvariantCulture)
 1222                //        };
 1223                //    })
 1224                //    .ToList();
 1225
 01226                var albumArtistsLookup = artistsBatch
 01227                    ?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(
 1228
 01229                dto.AlbumArtists = hasAlbumArtist.AlbumArtists
 01230                    .Where(name => !string.IsNullOrWhiteSpace(name))
 01231                    .Distinct()
 01232                    .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 
 01233                        ? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
 01234                        : null)
 01235                    .Where(item => item is not null)
 01236                    .ToArray();
 1237            }
 1238
 1239            // Add video info
 91240            if (item is Video video)
 1241            {
 01242                dto.VideoType = video.VideoType;
 01243                dto.Video3DFormat = video.Video3DFormat;
 01244                dto.IsoType = video.IsoType;
 1245
 01246                if (video.HasSubtitles)
 1247                {
 01248                    dto.HasSubtitles = video.HasSubtitles;
 1249                }
 1250
 01251                if (video.AdditionalParts.Length != 0)
 1252                {
 01253                    dto.PartCount = video.AdditionalParts.Length + 1;
 1254                }
 1255
 01256                if (options.ContainsField(ItemFields.MediaSourceCount))
 1257                {
 01258                    var mediaSourceCount = video.MediaSourceCount;
 01259                    if (mediaSourceCount != 1)
 1260                    {
 01261                        dto.MediaSourceCount = mediaSourceCount;
 1262                    }
 1263                }
 1264
 01265                if (options.ContainsField(ItemFields.Trickplay))
 1266                {
 01267                    var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
 01268                    dto.Trickplay = trickplay.ToDictionary(
 01269                        mediaStream => mediaStream.Key,
 01270                        mediaStream => mediaStream.Value.ToDictionary(
 01271                            width => width.Key,
 01272                            width => new TrickplayInfoDto(width.Value)));
 1273                }
 1274
 01275                dto.ExtraType = video.ExtraType;
 1276            }
 1277
 91278            if (options.ContainsField(ItemFields.Chapters))
 1279            {
 61280                dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
 1281            }
 1282
 91283            if (options.ContainsField(ItemFields.MediaStreams))
 1284            {
 1285                // Add VideoInfo
 61286                if (item is IHasMediaSources)
 1287                {
 1288                    MediaStream[] mediaStreams;
 1289
 01290                    if (dto.MediaSources is not null && dto.MediaSources.Length > 0)
 1291                    {
 01292                        if (item.SourceType == SourceType.Channel)
 1293                        {
 01294                            mediaStreams = dto.MediaSources[0].MediaStreams.ToArray();
 1295                        }
 1296                        else
 1297                        {
 01298                            string id = item.Id.ToString("N", CultureInfo.InvariantCulture);
 01299                            mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalI
 01300                                .SelectMany(i => i.MediaStreams)
 01301                                .ToArray();
 1302                        }
 1303                    }
 1304                    else
 1305                    {
 01306                        mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
 1307                    }
 1308
 01309                    dto.MediaStreams = mediaStreams;
 1310                }
 1311            }
 1312
 91313            BaseItem[]? allExtras = null;
 1314
 91315            if (options.ContainsField(ItemFields.SpecialFeatureCount))
 1316            {
 61317                allExtras = item.GetExtras().ToArray();
 61318                dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contai
 1319            }
 1320
 91321            if (options.ContainsField(ItemFields.LocalTrailerCount))
 1322            {
 61323                if (item is IHasTrailers hasTrailers)
 1324                {
 01325                    dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
 1326                }
 1327                else
 1328                {
 61329                    dto.LocalTrailerCount = (allExtras ?? item.GetExtras()).Count(i => i.ExtraType == ExtraType.Trailer)
 1330                }
 1331            }
 1332
 1333            // Add EpisodeInfo
 91334            if (item is Episode episode)
 1335            {
 01336                dto.IndexNumberEnd = episode.IndexNumberEnd;
 01337                dto.SeriesName = episode.SeriesName;
 1338
 01339                if (options.ContainsField(ItemFields.SpecialEpisodeNumbers))
 1340                {
 01341                    dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber;
 01342                    dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber;
 01343                    dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber;
 1344                }
 1345
 01346                dto.SeasonName = episode.SeasonName;
 01347                dto.SeasonId = episode.SeasonId;
 01348                dto.SeriesId = episode.SeriesId;
 1349
 01350                Series? episodeSeries = null;
 1351
 1352                // this block will add the series poster for episodes without a poster
 1353                // TODO maybe remove the if statement entirely
 1354                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
 1355                {
 01356                    episodeSeries ??= episode.Series;
 01357                    if (episodeSeries is not null)
 1358                    {
 01359                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
 01360                        if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
 1361                        {
 01362                            AttachPrimaryImageAspectRatio(dto, episodeSeries);
 1363                        }
 1364                    }
 1365                }
 1366
 01367                if (options.ContainsField(ItemFields.SeriesStudio))
 1368                {
 01369                    episodeSeries ??= episode.Series;
 01370                    if (episodeSeries is not null)
 1371                    {
 01372                        dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
 1373                    }
 1374                }
 1375            }
 1376
 1377            // Add SeriesInfo
 1378            Series? series;
 91379            if (item is Series tmp)
 1380            {
 01381                series = tmp;
 01382                dto.AirDays = series.AirDays;
 01383                dto.AirTime = series.AirTime;
 01384                dto.Status = series.Status?.ToString();
 1385            }
 1386
 1387            // Add SeasonInfo
 91388            if (item is Season season)
 1389            {
 01390                dto.SeriesName = season.SeriesName;
 01391                dto.SeriesId = season.SeriesId;
 1392
 01393                series = null;
 1394
 01395                if (options.ContainsField(ItemFields.SeriesStudio))
 1396                {
 01397                    series ??= season.Series;
 01398                    if (series is not null)
 1399                    {
 01400                        dto.SeriesStudio = series.Studios.FirstOrDefault();
 1401                    }
 1402                }
 1403
 1404                // this block will add the series poster for seasons without a poster
 1405                // TODO maybe remove the if statement entirely
 1406                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
 1407                {
 01408                    series ??= season.Series;
 01409                    if (series is not null)
 1410                    {
 01411                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
 01412                        if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
 1413                        {
 01414                            AttachPrimaryImageAspectRatio(dto, series);
 1415                        }
 1416                    }
 1417                }
 1418            }
 1419
 91420            if (item is MusicVideo musicVideo)
 1421            {
 01422                SetMusicVideoProperties(dto, musicVideo);
 1423            }
 1424
 91425            if (item is Book book)
 1426            {
 01427                SetBookProperties(dto, book);
 1428            }
 1429
 91430            if (options.ContainsField(ItemFields.ProductionLocations))
 1431            {
 61432                if (item.ProductionLocations.Length > 0 || item is Movie)
 1433                {
 01434                    dto.ProductionLocations = item.ProductionLocations;
 1435                }
 1436            }
 1437
 91438            if (options.ContainsField(ItemFields.Width))
 1439            {
 61440                var width = item.Width;
 61441                if (width > 0)
 1442                {
 01443                    dto.Width = width;
 1444                }
 1445            }
 1446
 91447            if (options.ContainsField(ItemFields.Height))
 1448            {
 61449                var height = item.Height;
 61450                if (height > 0)
 1451                {
 01452                    dto.Height = height;
 1453                }
 1454            }
 1455
 91456            if (options.ContainsField(ItemFields.IsHD))
 1457            {
 1458                // Compatibility
 61459                if (item.IsHD)
 1460                {
 01461                    dto.IsHD = true;
 1462                }
 1463            }
 1464
 91465            if (item is Photo photo)
 1466            {
 01467                SetPhotoProperties(dto, photo);
 1468            }
 1469
 91470            dto.ChannelId = item.ChannelId;
 1471
 91472            if (item.SourceType == SourceType.Channel)
 1473            {
 01474                var channel = _libraryManager.GetItemById(item.ChannelId);
 01475                if (channel is not null)
 1476                {
 01477                    dto.ChannelName = channel.Name;
 1478                }
 1479            }
 91480        }
 1481
 1482        private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
 1483        {
 01484            if (currentItem is MusicAlbum musicAlbum)
 1485            {
 01486                var artist = musicAlbum.GetMusicArtist(new DtoOptions(false));
 01487                if (artist is not null)
 1488                {
 01489                    return artist;
 1490                }
 1491            }
 1492
 01493            var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
 1494
 01495            if (parent is null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is 
 1496            {
 01497                parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
 1498            }
 1499
 01500            return parent;
 1501        }
 1502
 1503        private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
 1504        {
 91505            if (!item.SupportsInheritedParentImages)
 1506            {
 91507                return;
 1508            }
 1509
 01510            var logoLimit = options.GetImageLimit(ImageType.Logo);
 01511            var artLimit = options.GetImageLimit(ImageType.Art);
 01512            var thumbLimit = options.GetImageLimit(ImageType.Thumb);
 01513            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
 1514
 1515            // For now. Emby apps are not using this
 01516            artLimit = 0;
 1517
 01518            if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0)
 1519            {
 01520                return;
 1521            }
 1522
 01523            BaseItem? parent = null;
 01524            var isFirst = true;
 1525
 01526            var imageTags = dto.ImageTags;
 1527
 01528            while ((!(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
 01529                || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
 01530                || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
 01531                || parent is Series)
 1532            {
 01533                parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
 01534                if (parent is null)
 1535                {
 1536                    break;
 1537                }
 1538
 01539                var allImages = parent.ImageInfos;
 1540
 01541                if (logoLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogo
 1542                {
 01543                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
 1544
 01545                    if (image is not null)
 1546                    {
 01547                        dto.ParentLogoItemId = parent.Id;
 01548                        dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1549                    }
 1550                }
 1551
 01552                if (artLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtIte
 1553                {
 01554                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
 1555
 01556                    if (image is not null)
 1557                    {
 01558                        dto.ParentArtItemId = parent.Id;
 01559                        dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1560                    }
 1561                }
 1562
 01563                if (thumbLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentT
 1564                {
 01565                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
 1566
 01567                    if (image is not null)
 1568                    {
 01569                        dto.ParentThumbItemId = parent.Id;
 01570                        dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1571                    }
 1572                }
 1573
 01574                if (backdropLimit > 0 && !((dto.BackdropImageTags is not null && dto.BackdropImageTags.Length > 0) || (d
 1575                {
 01576                    var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
 1577
 01578                    if (images.Count > 0)
 1579                    {
 01580                        dto.ParentBackdropItemId = parent.Id;
 01581                        dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
 1582                    }
 1583                }
 1584
 01585                isFirst = false;
 1586
 01587                if (!parent.SupportsInheritedParentImages)
 1588                {
 1589                    break;
 1590                }
 1591
 01592                parent = GetImageDisplayParent(parent, item);
 1593            }
 01594        }
 1595
 1596        private string GetMappedPath(BaseItem item, BaseItem? ownerItem)
 1597        {
 61598            var path = item.Path;
 1599
 61600            if (item.IsFileProtocol)
 1601            {
 61602                path = _libraryManager.GetPathAfterNetworkSubstitution(path, ownerItem ?? item);
 1603            }
 1604
 61605            return path;
 1606        }
 1607
 1608        /// <summary>
 1609        /// Attaches the primary image aspect ratio.
 1610        /// </summary>
 1611        /// <param name="dto">The dto.</param>
 1612        /// <param name="item">The item.</param>
 1613        public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
 1614        {
 61615            dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
 61616        }
 1617
 1618        public double? GetPrimaryImageAspectRatio(BaseItem item)
 1619        {
 61620            var imageInfo = item.GetImageInfo(ImageType.Primary, 0);
 1621
 61622            if (imageInfo is null)
 1623            {
 61624                return null;
 1625            }
 1626
 01627            if (!imageInfo.IsLocalFile)
 1628            {
 01629                return item.GetDefaultPrimaryImageAspectRatio();
 1630            }
 1631
 1632            try
 1633            {
 01634                var size = _imageProcessor.GetImageDimensions(item, imageInfo);
 01635                var width = size.Width;
 01636                var height = size.Height;
 01637                if (width > 0 && height > 0)
 1638                {
 01639                    return (double)width / height;
 1640                }
 01641            }
 01642            catch (Exception ex)
 1643            {
 01644                _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
 01645            }
 1646
 01647            return item.GetDefaultPrimaryImageAspectRatio();
 01648        }
 1649    }
 1650}

Methods/Properties

.cctor()
.ctor(Microsoft.Extensions.Logging.ILogger`1<Emby.Server.Implementations.Dto.DtoService>,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Library.IUserDataManager,MediaBrowser.Controller.Drawing.IImageProcessor,MediaBrowser.Controller.Providers.IProviderManager,MediaBrowser.Controller.LiveTv.IRecordingsManager,MediaBrowser.Common.IApplicationHost,MediaBrowser.Controller.Library.IMediaSourceManager,System.Lazy`1<MediaBrowser.Controller.LiveTv.ILiveTvManager>,MediaBrowser.Controller.Trickplay.ITrickplayManager,MediaBrowser.Controller.Chapters.IChapterManager)
get_LivetvManager()
GetBaseItemDtos(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Dto.DtoOptions,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
GetBaseItemDto(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Dto.DtoOptions,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
GetBaseItemDtoInternal(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Dto.DtoOptions,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.UserItemData,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>,System.Collections.Generic.Dictionary`2<System.Guid,System.Int32>,System.Collections.Generic.Dictionary`2<System.Guid,System.ValueTuple`2<System.Int32,System.Int32>>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,MediaBrowser.Controller.Entities.Audio.MusicArtist[]>)
NormalizeMediaSourceContainers(MediaBrowser.Model.Dto.BaseItemDto)
GetItemByNameDto(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Dto.DtoOptions,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User)
SetItemByNameInfo(MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Database.Implementations.Entities.User)
SetItemByNameInfo(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.BaseItemDto,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
AttachUserSpecificInfo(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions,MediaBrowser.Controller.Entities.UserItemData,System.Collections.Generic.Dictionary`2<System.Guid,System.Int32>,System.Collections.Generic.Dictionary`2<System.Guid,System.ValueTuple`2<System.Int32,System.Int32>>)
GetUserItemDataDto(MediaBrowser.Controller.Entities.UserItemData,System.Guid)
GetChildCount(MediaBrowser.Controller.Entities.Folder,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.Dictionary`2<System.Guid,System.Int32>)
SetBookProperties(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.Book)
SetPhotoProperties(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.Photo)
SetMusicVideoProperties(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.MusicVideo)
GetImageTags(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.ItemImageInfo>)
GetImageCacheTag(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.ItemImageInfo)
AttachPeople(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
AttachStudios(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem)
AttachGenreItems(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem)
GetGenreId(System.String,MediaBrowser.Controller.Entities.BaseItem)
GetTagAndFillBlurhash(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Entities.ImageType,System.Int32)
GetTagAndFillBlurhash(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.ItemImageInfo)
GetTagsAndFillBlurhashes(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Entities.ImageType,System.Int32)
GetTagsAndFillBlurhashes(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Entities.ImageType,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.ItemImageInfo>)
AttachBasicFields(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Dto.DtoOptions,System.Collections.Generic.IReadOnlyDictionary`2<System.String,MediaBrowser.Controller.Entities.Audio.MusicArtist[]>)
GetImageDisplayParent(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
AddInheritedImages(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Dto.DtoOptions,MediaBrowser.Controller.Entities.BaseItem)
GetMappedPath(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
AttachPrimaryImageAspectRatio(MediaBrowser.Model.Dto.IItemDto,MediaBrowser.Controller.Entities.BaseItem)
GetPrimaryImageAspectRatio(MediaBrowser.Controller.Entities.BaseItem)