< 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
73%
Covered lines: 643
Uncovered lines: 234
Coverable lines: 877
Total lines: 1881
Line coverage: 73.3%
Branch coverage
57%
Covered branches: 369
Total branches: 644
Branch coverage: 57.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/14/2026 - 12:15:54 AM Line coverage: 53.5% (412/770) Branch coverage: 38.1% (199/522) Total lines: 16525/20/2026 - 12:15:44 AM Line coverage: 53.5% (412/770) Branch coverage: 31.9% (167/522) Total lines: 16526/2/2026 - 12:15:49 AM Line coverage: 63.9% (505/790) Branch coverage: 44.4% (241/542) Total lines: 16876/8/2026 - 12:16:15 AM Line coverage: 64.3% (514/799) Branch coverage: 46.2% (258/558) Total lines: 17026/30/2026 - 12:15:32 AM Line coverage: 64.4% (516/801) Branch coverage: 46.2% (258/558) Total lines: 17047/6/2026 - 12:16:28 AM Line coverage: 64.2% (520/809) Branch coverage: 45.4% (259/570) Total lines: 17217/18/2026 - 12:15:19 AM Line coverage: 63.8% (511/800) Branch coverage: 45.3% (254/560) Total lines: 17057/27/2026 - 12:16:14 AM Line coverage: 67.9% (548/806) Branch coverage: 48% (275/572) Total lines: 17188/9/2026 - 12:16:58 AM Line coverage: 70% (572/816) Branch coverage: 51.3% (299/582) Total lines: 17458/11/2026 - 12:17:00 AM Line coverage: 71.1% (588/826) Branch coverage: 52.3% (310/592) Total lines: 177210/29/2026 - 12:17:11 AM Line coverage: 73.3% (643/877) Branch coverage: 57.2% (369/644) Total lines: 1881 5/14/2026 - 12:15:54 AM Line coverage: 53.5% (412/770) Branch coverage: 38.1% (199/522) Total lines: 16525/20/2026 - 12:15:44 AM Line coverage: 53.5% (412/770) Branch coverage: 31.9% (167/522) Total lines: 16526/2/2026 - 12:15:49 AM Line coverage: 63.9% (505/790) Branch coverage: 44.4% (241/542) Total lines: 16876/8/2026 - 12:16:15 AM Line coverage: 64.3% (514/799) Branch coverage: 46.2% (258/558) Total lines: 17026/30/2026 - 12:15:32 AM Line coverage: 64.4% (516/801) Branch coverage: 46.2% (258/558) Total lines: 17047/6/2026 - 12:16:28 AM Line coverage: 64.2% (520/809) Branch coverage: 45.4% (259/570) Total lines: 17217/18/2026 - 12:15:19 AM Line coverage: 63.8% (511/800) Branch coverage: 45.3% (254/560) Total lines: 17057/27/2026 - 12:16:14 AM Line coverage: 67.9% (548/806) Branch coverage: 48% (275/572) Total lines: 17188/9/2026 - 12:16:58 AM Line coverage: 70% (572/816) Branch coverage: 51.3% (299/582) Total lines: 17458/11/2026 - 12:17:00 AM Line coverage: 71.1% (588/826) Branch coverage: 52.3% (310/592) Total lines: 177210/29/2026 - 12:17:11 AM Line coverage: 73.3% (643/877) Branch coverage: 57.2% (369/644) Total lines: 1881

Coverage delta

Coverage delta 13 -13

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;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Database.Implementations.Entities;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Common;
 14using MediaBrowser.Controller.Channels;
 15using MediaBrowser.Controller.Chapters;
 16using MediaBrowser.Controller.Drawing;
 17using MediaBrowser.Controller.Dto;
 18using MediaBrowser.Controller.Entities;
 19using MediaBrowser.Controller.Entities.Audio;
 20using MediaBrowser.Controller.Library;
 21using MediaBrowser.Controller.LiveTv;
 22using MediaBrowser.Controller.Playlists;
 23using MediaBrowser.Controller.Providers;
 24using MediaBrowser.Controller.Trickplay;
 25using MediaBrowser.Model.Dto;
 26using MediaBrowser.Model.Entities;
 27using MediaBrowser.Model.Querying;
 28using Microsoft.Extensions.Logging;
 29using Book = MediaBrowser.Controller.Entities.Book;
 30using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 31using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 32using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 33using Person = MediaBrowser.Controller.Entities.Person;
 34using Photo = MediaBrowser.Controller.Entities.Photo;
 35using Season = MediaBrowser.Controller.Entities.TV.Season;
 36using Series = MediaBrowser.Controller.Entities.TV.Series;
 37
 38namespace Emby.Server.Implementations.Dto
 39{
 40    public class DtoService : IDtoService
 41    {
 142        private static readonly FrozenDictionary<BaseItemKind, BaseItemKind[]> _relatedItemKinds = new Dictionary<BaseIt
 143        {
 144            {
 145                BaseItemKind.Genre, [
 146                    BaseItemKind.Audio,
 147                    BaseItemKind.Episode,
 148                    BaseItemKind.Movie,
 149                    BaseItemKind.LiveTvProgram,
 150                    BaseItemKind.MusicAlbum,
 151                    BaseItemKind.MusicArtist,
 152                    BaseItemKind.MusicVideo,
 153                    BaseItemKind.Series,
 154                    BaseItemKind.Trailer
 155                ]
 156            },
 157            {
 158                BaseItemKind.MusicArtist, [
 159                    BaseItemKind.Audio,
 160                    BaseItemKind.MusicAlbum,
 161                    BaseItemKind.MusicVideo
 162                ]
 163            },
 164            {
 165                BaseItemKind.MusicGenre, [
 166                    BaseItemKind.Audio,
 167                    BaseItemKind.MusicAlbum,
 168                    BaseItemKind.MusicArtist,
 169                    BaseItemKind.MusicVideo
 170                ]
 171            },
 172            {
 173                BaseItemKind.Person, [
 174                    BaseItemKind.Audio,
 175                    BaseItemKind.AudioBook,
 176                    BaseItemKind.Book,
 177                    BaseItemKind.Episode,
 178                    BaseItemKind.Movie,
 179                    BaseItemKind.LiveTvProgram,
 180                    BaseItemKind.MusicAlbum,
 181                    BaseItemKind.MusicArtist,
 182                    BaseItemKind.MusicVideo,
 183                    BaseItemKind.Series,
 184                    BaseItemKind.Trailer
 185                ]
 186            },
 187            {
 188                BaseItemKind.Studio, [
 189                    BaseItemKind.Audio,
 190                    BaseItemKind.Episode,
 191                    BaseItemKind.Movie,
 192                    BaseItemKind.LiveTvProgram,
 193                    BaseItemKind.MusicAlbum,
 194                    BaseItemKind.MusicArtist,
 195                    BaseItemKind.MusicVideo,
 196                    BaseItemKind.Series,
 197                    BaseItemKind.Trailer
 198                ]
 199            },
 1100            {
 1101                BaseItemKind.Year, [
 1102                    BaseItemKind.Audio,
 1103                    BaseItemKind.Episode,
 1104                    BaseItemKind.Movie,
 1105                    BaseItemKind.LiveTvProgram,
 1106                    BaseItemKind.MusicAlbum,
 1107                    BaseItemKind.MusicArtist,
 1108                    BaseItemKind.MusicVideo,
 1109                    BaseItemKind.Series,
 1110                    BaseItemKind.Trailer
 1111                ]
 1112            }
 1113        }.ToFrozenDictionary();
 114
 115        private readonly ILogger<DtoService> _logger;
 116        private readonly ILibraryManager _libraryManager;
 117        private readonly IUserDataManager _userDataRepository;
 118
 119        private readonly IImageProcessor _imageProcessor;
 120        private readonly IProviderManager _providerManager;
 121        private readonly IRecordingsManager _recordingsManager;
 122
 123        private readonly IApplicationHost _appHost;
 124        private readonly IMediaSourceManager _mediaSourceManager;
 125        private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 126
 127        private readonly ITrickplayManager _trickplayManager;
 128        private readonly IChapterManager _chapterManager;
 129
 130        public DtoService(
 131            ILogger<DtoService> logger,
 132            ILibraryManager libraryManager,
 133            IUserDataManager userDataRepository,
 134            IImageProcessor imageProcessor,
 135            IProviderManager providerManager,
 136            IRecordingsManager recordingsManager,
 137            IApplicationHost appHost,
 138            IMediaSourceManager mediaSourceManager,
 139            Lazy<ILiveTvManager> livetvManagerFactory,
 140            ITrickplayManager trickplayManager,
 141            IChapterManager chapterManager)
 142        {
 41143            _logger = logger;
 41144            _libraryManager = libraryManager;
 41145            _userDataRepository = userDataRepository;
 41146            _imageProcessor = imageProcessor;
 41147            _providerManager = providerManager;
 41148            _recordingsManager = recordingsManager;
 41149            _appHost = appHost;
 41150            _mediaSourceManager = mediaSourceManager;
 41151            _livetvManagerFactory = livetvManagerFactory;
 41152            _trickplayManager = trickplayManager;
 41153            _chapterManager = chapterManager;
 41154        }
 155
 0156        private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
 157
 158        /// <inheritdoc />
 159        public IReadOnlyList<BaseItemDto> GetBaseItemDtos(
 160            IReadOnlyList<BaseItem> items,
 161            DtoOptions options,
 162            User? user = null,
 163            BaseItem? owner = null,
 164            bool skipVisibilityCheck = false)
 165        {
 13166            var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToLi
 13167            var returnItems = new BaseItemDto[accessibleItems.Count];
 13168            List<(BaseItem, BaseItemDto)>? programTuples = null;
 13169            List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
 170
 171            // Batch-fetch user data for all items
 13172            Dictionary<Guid, UserItemData>? userDataBatch = null;
 13173            IReadOnlyDictionary<Guid, VersionResumeData>? resumeDataBatch = null;
 13174            if (user is not null && options.EnableUserData)
 175            {
 8176                userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
 177
 178                // For items with alternate versions, the most recently played version drives resume.
 8179                resumeDataBatch = _userDataRepository.GetResumeUserDataBatch(accessibleItems, user);
 180            }
 181
 182            // Pre-compute collection folders once to avoid N+1 queries in CanDelete
 13183            List<Folder>? allCollectionFolders = null;
 13184            if (user is not null && options.ContainsField(ItemFields.CanDelete))
 185            {
 0186                allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 187            }
 188
 189            // Batch-fetch by-name item counts to avoid N+1 queries
 13190            Dictionary<Guid, ItemCounts>? itemCountsBatch = null;
 13191            if (options.ContainsField(ItemFields.ItemCounts))
 192            {
 0193                itemCountsBatch = GetItemCountsBatch(accessibleItems, user);
 194            }
 195
 196            // Batch-fetch child counts for all folders to avoid N+1 queries.
 13197            Dictionary<Guid, int>? childCountBatch = null;
 13198            if (user is not null && options.ContainsField(ItemFields.ChildCount))
 199            {
 4200                childCountBatch = GetChildCountBatch(accessibleItems, user);
 201            }
 202
 203            // Batch-fetch played/total counts for all folders to avoid N+1 queries
 13204            Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null;
 13205            if (user is not null && options.EnableUserData)
 206            {
 8207                var folderIds = accessibleItems.OfType<Folder>()
 8208                    .Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemF
 8209                    .Select(f => f.Id).ToList();
 8210                if (folderIds.Count > 0)
 211                {
 2212                    playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
 213                }
 214            }
 215
 216            // Batch-fetch MusicArtist lookups across all items to avoid N+1 queries.
 13217            IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null;
 13218            var artistNames = new HashSet<string>(StringComparer.Ordinal);
 56219            foreach (var item in accessibleItems)
 220            {
 15221                if (item is IHasArtist hasArtist)
 222                {
 8223                    foreach (var name in hasArtist.Artists)
 224                    {
 0225                        if (!string.IsNullOrWhiteSpace(name))
 226                        {
 0227                            artistNames.Add(name);
 228                        }
 229                    }
 230                }
 231
 15232                if (item is IHasAlbumArtist hasAlbumArtist)
 233                {
 12234                    foreach (var name in hasAlbumArtist.AlbumArtists)
 235                    {
 2236                        if (!string.IsNullOrWhiteSpace(name))
 237                        {
 2238                            artistNames.Add(name);
 239                        }
 240                    }
 241                }
 242            }
 243
 13244            if (artistNames.Count > 0)
 245            {
 1246                artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
 247            }
 248
 249            // Batch-fetch people across all items to avoid one GetPeople query per item.
 13250            IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null;
 13251            if (options.ContainsField(ItemFields.People))
 252            {
 1253                var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList();
 1254                if (peopleItemIds.Count > 0)
 255                {
 1256                    peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds);
 257                }
 258            }
 259
 260            // Batch-detect which videos own alternate versions to avoid the per-item alternate-version
 261            // queries in MediaSourceCount. Videos absent from this set have a single media source.
 13262            IReadOnlySet<Guid>? alternateVersionItemIds = null;
 13263            if (options.ContainsField(ItemFields.MediaSourceCount))
 264            {
 2265                var versionItemIds = accessibleItems.OfType<Video>().Select(i => i.Id).ToList();
 2266                if (versionItemIds.Count > 0)
 267                {
 2268                    alternateVersionItemIds = _libraryManager.GetItemIdsWithAlternateVersions(versionItemIds);
 269                }
 270            }
 271
 56272            for (int index = 0; index < accessibleItems.Count; index++)
 273            {
 15274                var item = accessibleItems[index];
 15275                var dto = GetBaseItemDtoInternal(
 15276                    item,
 15277                    options,
 15278                    user,
 15279                    owner,
 15280                    userDataBatch?.GetValueOrDefault(item.Id),
 15281                    allCollectionFolders,
 15282                    childCountBatch,
 15283                    playedCountBatch,
 15284                    artistsBatch,
 15285                    resumeDataBatch?.GetValueOrDefault(item.Id),
 15286                    peopleBatch,
 15287                    alternateVersionItemIds);
 288
 15289                if (item is LiveTvChannel tvChannel)
 290                {
 0291                    (channelTuples ??= []).Add((dto, tvChannel));
 292                }
 15293                else if (item is LiveTvProgram)
 294                {
 0295                    (programTuples ??= []).Add((item, dto));
 296                }
 297
 15298                if (options.ContainsField(ItemFields.ItemCounts))
 299                {
 0300                    SetItemByNameInfo(dto, user, itemCountsBatch);
 301                }
 302
 15303                returnItems[index] = dto;
 304            }
 305
 13306            if (programTuples is not null)
 307            {
 0308                LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
 309            }
 310
 13311            if (channelTuples is not null)
 312            {
 0313                LivetvManager.AddChannelInfo(channelTuples, options, user);
 314            }
 315
 13316            return returnItems;
 317        }
 318
 319        public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
 320        {
 11321            var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
 11322            if (item is LiveTvChannel tvChannel)
 323            {
 0324                LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
 325            }
 11326            else if (item is LiveTvProgram)
 327            {
 0328                LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
 329            }
 330
 11331            if (options.ContainsField(ItemFields.ItemCounts))
 332            {
 6333                SetItemByNameInfo(dto, user);
 334            }
 335
 11336            return dto;
 337        }
 338
 339        private BaseItemDto GetBaseItemDtoInternal(
 340            BaseItem item,
 341            DtoOptions options,
 342            User? user = null,
 343            BaseItem? owner = null,
 344            UserItemData? userData = null,
 345            List<Folder>? allCollectionFolders = null,
 346            Dictionary<Guid, int>? childCountBatch = null,
 347            Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
 348            IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
 349            VersionResumeData? resumeData = null,
 350            IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null,
 351            IReadOnlySet<Guid>? alternateVersionItemIds = null)
 352        {
 26353            var dto = new BaseItemDto
 26354            {
 26355                ServerId = _appHost.SystemId
 26356            };
 357
 26358            if (item.SourceType == SourceType.Channel)
 359            {
 0360                dto.SourceType = item.SourceType.ToString();
 361            }
 362
 26363            if (options.ContainsField(ItemFields.People))
 364            {
 8365                IReadOnlyList<PersonInfo>? prefetchedPeople = null;
 8366                if (peopleBatch is not null)
 367                {
 368                    // The batch omits items with no people, so a miss means "no people",
 369                    // not "not fetched". Use an empty list to skip the per-item query.
 2370                    prefetchedPeople = peopleBatch.GetValueOrDefault(item.Id) ?? [];
 371                }
 372
 8373                AttachPeople(dto, item, user, prefetchedPeople);
 374            }
 375
 26376            if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
 377            {
 378                try
 379                {
 7380                    AttachPrimaryImageAspectRatio(dto, item);
 7381                }
 0382                catch (Exception ex)
 383                {
 384                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 0385                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
 0386                }
 387            }
 388
 26389            if (options.ContainsField(ItemFields.DisplayPreferencesId))
 390            {
 6391                dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture);
 392            }
 393
 26394            if (user is not null)
 395            {
 13396                AttachUserSpecificInfo(
 13397                    dto,
 13398                    item,
 13399                    user,
 13400                    options,
 13401                    userData,
 13402                    childCountBatch,
 13403                    playedCountBatch,
 13404                    resumeData);
 405            }
 406
 26407            if (item is IHasMediaSources
 26408                && options.ContainsField(ItemFields.MediaSources))
 409            {
 0410                dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray();
 411
 0412                NormalizeMediaSourceContainers(dto);
 413            }
 414
 26415            if (options.ContainsField(ItemFields.Studios))
 416            {
 6417                AttachStudios(dto, item);
 418            }
 419
 26420            AttachBasicFields(dto, item, owner, options, artistsBatch, user, alternateVersionItemIds);
 421
 26422            if (options.ContainsField(ItemFields.CanDelete))
 423            {
 6424                dto.CanDelete = user is null
 6425                    ? item.CanDelete()
 6426                    : allCollectionFolders is not null
 6427                        ? item.CanDelete(user, allCollectionFolders)
 6428                        : item.CanDelete(user);
 429            }
 430
 26431            if (options.ContainsField(ItemFields.CanDownload))
 432            {
 6433                dto.CanDownload = user is null
 6434                    ? item.CanDownload()
 6435                    : item.CanDownload(user);
 436            }
 437
 26438            if (options.ContainsField(ItemFields.Etag))
 439            {
 6440                dto.Etag = item.GetEtag(user);
 441            }
 442
 26443            var activeRecording = _recordingsManager.GetActiveRecordingInfo(item.Path);
 26444            if (activeRecording is not null)
 445            {
 0446                dto.Type = BaseItemKind.Recording;
 0447                dto.CanDownload = false;
 0448                dto.RunTimeTicks = null;
 449
 0450                if (!string.IsNullOrEmpty(dto.SeriesName))
 451                {
 0452                    dto.EpisodeTitle = dto.Name;
 0453                    dto.Name = dto.SeriesName;
 454                }
 455
 0456                LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
 457            }
 458
 26459            if (item is Audio audio)
 460            {
 0461                dto.HasLyrics = audio.GetMediaStreams().Any(s => s.Type == MediaStreamType.Lyric);
 462            }
 463
 26464            return dto;
 465        }
 466
 467        private static void NormalizeMediaSourceContainers(BaseItemDto dto)
 468        {
 0469            foreach (var mediaSource in dto.MediaSources)
 470            {
 0471                var container = mediaSource.Container;
 0472                if (string.IsNullOrEmpty(container))
 473                {
 474                    continue;
 475                }
 476
 0477                var containers = container.Split(',');
 0478                if (containers.Length < 2)
 479                {
 480                    continue;
 481                }
 482
 0483                var path = mediaSource.Path;
 0484                string? fileExtensionContainer = null;
 485
 0486                if (!string.IsNullOrEmpty(path))
 487                {
 0488                    path = Path.GetExtension(path);
 0489                    if (!string.IsNullOrEmpty(path))
 490                    {
 0491                        path = Path.GetExtension(path);
 0492                        if (!string.IsNullOrEmpty(path))
 493                        {
 0494                            path = path.TrimStart('.');
 495                        }
 496
 0497                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase)
 498                        {
 0499                            fileExtensionContainer = path;
 500                        }
 501                    }
 502                }
 503
 0504                mediaSource.Container = fileExtensionContainer ?? containers[0];
 505            }
 0506        }
 507
 508        /// <inheritdoc />
 509        /// TODO refactor this to use the new SetItemByNameInfo.
 510        /// Some callers already have the counts extracted so no reason to retrieve them again.
 511        public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user =
 512        {
 0513            var dto = GetBaseItemDtoInternal(item, options, user);
 514
 0515            if (options.ContainsField(ItemFields.ItemCounts)
 0516                && taggedItems is not null
 0517                && taggedItems.Count != 0)
 518            {
 0519                SetItemByNameInfo(item, dto, taggedItems);
 520            }
 521
 0522            return dto;
 523        }
 524
 525        private Dictionary<Guid, ItemCounts> GetItemCountsBatch(IReadOnlyList<BaseItem> items, User? user)
 526        {
 0527            var result = new Dictionary<Guid, ItemCounts>();
 528
 0529            foreach (var group in items.GroupBy(item => item.GetBaseItemKind()))
 530            {
 0531                if (!_relatedItemKinds.TryGetValue(group.Key, out var relatedItemKinds))
 532                {
 533                    continue;
 534                }
 535
 0536                var ids = group.Select(item => item.Id).ToArray();
 0537                foreach (var (id, counts) in _libraryManager.GetItemCountsForNameItems(group.Key, ids, relatedItemKinds,
 538                {
 0539                    result[id] = counts;
 540                }
 541            }
 542
 0543            return result;
 544        }
 545
 546        private void SetItemByNameInfo(BaseItemDto dto, User? user, IReadOnlyDictionary<Guid, ItemCounts>? prefetchedCou
 547        {
 6548            if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
 549            {
 6550                return;
 551            }
 552
 0553            var counts = prefetchedCounts?.GetValueOrDefault(dto.Id)
 0554                ?? _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
 555
 0556            dto.AlbumCount = counts.AlbumCount;
 0557            dto.ArtistCount = counts.ArtistCount;
 0558            dto.EpisodeCount = counts.EpisodeCount;
 0559            dto.MovieCount = counts.MovieCount;
 0560            dto.MusicVideoCount = counts.MusicVideoCount;
 0561            dto.ProgramCount = counts.ProgramCount;
 0562            dto.SeriesCount = counts.SeriesCount;
 0563            dto.SongCount = counts.SongCount;
 0564            dto.TrailerCount = counts.TrailerCount;
 0565            dto.ChildCount = counts.TotalItemCount();
 0566        }
 567
 568        private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
 569        {
 0570            if (item is MusicArtist)
 571            {
 0572                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0573                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0574                dto.SongCount = taggedItems.Count(i => i is Audio);
 575            }
 0576            else if (item is MusicGenre)
 577            {
 0578                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
 0579                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0580                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0581                dto.SongCount = taggedItems.Count(i => i is Audio);
 582            }
 583            else
 584            {
 585                // This populates them all and covers Genre, Person, Studio, Year
 586
 0587                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
 0588                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0589                dto.EpisodeCount = taggedItems.Count(i => i is Episode);
 0590                dto.MovieCount = taggedItems.Count(i => i is Movie);
 0591                dto.TrailerCount = taggedItems.Count(i => i is Trailer);
 0592                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0593                dto.SeriesCount = taggedItems.Count(i => i is Series);
 0594                dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram);
 0595                dto.SongCount = taggedItems.Count(i => i is Audio);
 596            }
 597
 0598            dto.ChildCount = taggedItems.Count;
 0599        }
 600
 601        /// <summary>
 602        /// Attaches the user specific info.
 603        /// </summary>
 604        private void AttachUserSpecificInfo(
 605            BaseItemDto dto,
 606            BaseItem item,
 607            User user,
 608            DtoOptions options,
 609            UserItemData? userData = null,
 610            Dictionary<Guid, int>? childCountBatch = null,
 611            Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
 612            VersionResumeData? resumeData = null)
 613        {
 13614            if (item.IsFolder)
 615            {
 13616                var folder = (Folder)item;
 617
 13618                if (options.EnableUserData)
 619                {
 13620                    if (userData is not null)
 621                    {
 622                        // Use pre-fetched user data
 5623                        dto.UserData = GetUserItemDataDto(userData, item.Id);
 5624                        (int Played, int Total)? precomputed = playedCountBatch is not null
 5625                            && playedCountBatch.TryGetValue(item.Id, out var counts) ? counts : null;
 5626                        item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options, precomputed);
 627                    }
 628                    else
 629                    {
 630                        // Fall back to individual fetch
 8631                        dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
 632                    }
 633                }
 634
 13635                if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
 636                {
 637                    // For these types we can try to optimize and assume these values will be equal
 13638                    if (item is MusicAlbum || item is Season || item is Playlist)
 639                    {
 2640                        if (dto.RecursiveItemCount > 0)
 641                        {
 1642                            dto.ChildCount = dto.RecursiveItemCount;
 643                        }
 644
 2645                        var folderChildCount = folder.LinkedChildren.Length;
 646                        // The default is an empty array, so we can't reliably use the count when it's empty
 2647                        if (folderChildCount > 0)
 648                        {
 0649                            dto.ChildCount ??= folderChildCount;
 650                        }
 651                    }
 652
 13653                    if (options.ContainsField(ItemFields.ChildCount))
 654                    {
 10655                        dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
 656                    }
 657                }
 658
 13659                if (options.ContainsField(ItemFields.CumulativeRunTimeTicks))
 660                {
 6661                    dto.CumulativeRunTimeTicks = item.RunTimeTicks;
 662                }
 663
 13664                if (options.ContainsField(ItemFields.DateLastMediaAdded))
 665                {
 6666                    dto.DateLastMediaAdded = folder.DateLastMediaAdded;
 667                }
 668            }
 669            else
 670            {
 0671                if (options.EnableUserData)
 672                {
 0673                    if (userData is not null)
 674                    {
 675                        // Use pre-fetched user data
 0676                        dto.UserData = GetUserItemDataDto(userData, item.Id);
 0677                        item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
 678
 679                        // For items with alternate versions, the most recently played version drives resume.
 0680                        resumeData?.ApplyTo(dto.UserData);
 681                    }
 682                    else
 683                    {
 684                        // Fall back to individual fetch
 0685                        dto.UserData = _userDataRepository.GetUserDataDto(item, user);
 686                    }
 687                }
 688            }
 689
 13690            if (options.ContainsField(ItemFields.PlayAccess))
 691            {
 6692                dto.PlayAccess = item.GetPlayAccess(user);
 693            }
 13694        }
 695
 696        private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
 697        {
 5698            ArgumentNullException.ThrowIfNull(data);
 699
 5700            return new UserItemDataDto
 5701            {
 5702                IsFavorite = data.IsFavorite,
 5703                Likes = data.Likes,
 5704                PlaybackPositionTicks = data.PlaybackPositionTicks,
 5705                PlayCount = data.PlayCount,
 5706                Rating = data.Rating,
 5707                Played = data.Played,
 5708                LastPlayedDate = data.LastPlayedDate,
 5709                ItemId = itemId,
 5710                Key = data.Key
 5711            };
 712        }
 713
 714        private Dictionary<Guid, int>? GetChildCountBatch(IReadOnlyList<BaseItem> items, User user)
 715        {
 11716            Dictionary<Guid, IReadOnlyList<Guid>>? sources = null;
 44717            foreach (var folder in items.OfType<Folder>())
 718            {
 11719                var sourceIds = GetChildCountSourceIds(folder, user);
 11720                if (sourceIds.Count > 0)
 721                {
 9722                    (sources ??= new Dictionary<Guid, IReadOnlyList<Guid>>())[folder.Id] = sourceIds;
 723                }
 724            }
 725
 11726            if (sources is null)
 727            {
 2728                return null;
 729            }
 730
 9731            var counts = _libraryManager.GetChildCountBatch(
 9732                sources.Values.SelectMany(ids => ids).Distinct().ToList(),
 9733                user);
 734
 9735            var result = new Dictionary<Guid, int>(sources.Count);
 36736            foreach (var (folderId, sourceIds) in sources)
 737            {
 9738                var total = 0;
 42739                foreach (var sourceId in sourceIds)
 740                {
 12741                    total += counts.GetValueOrDefault(sourceId);
 742                }
 743
 9744                result[folderId] = total;
 745            }
 746
 9747            return result;
 748        }
 749
 750        private IReadOnlyList<Guid> GetChildCountSourceIds(Folder folder, User user)
 751        {
 11752            if (folder is CollectionFolder collectionFolder)
 753            {
 0754                return collectionFolder.PhysicalFolderIds;
 755            }
 756
 11757            if (folder is not UserView view)
 758            {
 8759                return [folder.Id];
 760            }
 761
 762            // Only a view that stands for a library proxies it. The sub-views a movie or show view
 763            // is built from hang off the same library but hold a query, not the library's children.
 3764            if (!UserView.EnableOriginalFolder(view.ViewType)
 3765                && view.ViewType is not (CollectionType.movies or CollectionType.tvshows))
 766            {
 2767                return [];
 768            }
 769
 770            // A view over a single library proxies that library, whatever the view type.
 1771            var parentId = view.DisplayParentId.IsEmpty() ? view.ParentId : view.DisplayParentId;
 1772            if (!parentId.IsEmpty()
 1773                && !parentId.Equals(view.Id)
 1774                && _libraryManager.GetItemById(parentId) is Folder parent
 1775                && parent is not UserView)
 776            {
 0777                return GetChildCountSourceIds(parent, user);
 778            }
 779
 780            // A grouped view has no single parent: it stands for every library the user grouped
 781            // into it, the same set UserViewManager builds the view from.
 1782            if (view.ViewType is CollectionType.movies or CollectionType.tvshows)
 783            {
 1784                return _libraryManager.GetUserRootFolder()
 1785                    .GetChildren(user, true)
 1786                    .OfType<CollectionFolder>()
 1787                    .Where(f => user.IsFolderGrouped(f.Id)
 1788                        && (f.CollectionType == view.ViewType || f.CollectionType is null))
 1789                    .SelectMany(f => f.PhysicalFolderIds)
 1790                    .Distinct()
 1791                    .ToList();
 792            }
 793
 0794            return [];
 795        }
 796
 797        private int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
 798        {
 799            // Use pre-fetched batch data if available
 9800            if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
 801            {
 2802                return count;
 803            }
 804
 805            // No batch covered this folder.
 7806            var single = GetChildCountBatch([folder], user);
 7807            return single is not null && single.TryGetValue(folder.Id, out var singleCount)
 7808                ? singleCount
 7809                : folder.GetChildCount(user);
 810        }
 811
 812        private static void SetBookProperties(BaseItemDto dto, Book item)
 813        {
 0814            dto.SeriesName = item.SeriesName;
 0815        }
 816
 817        private static void SetPhotoProperties(BaseItemDto dto, Photo item)
 818        {
 0819            dto.CameraMake = item.CameraMake;
 0820            dto.CameraModel = item.CameraModel;
 0821            dto.Software = item.Software;
 0822            dto.ExposureTime = item.ExposureTime;
 0823            dto.FocalLength = item.FocalLength;
 0824            dto.ImageOrientation = item.Orientation;
 0825            dto.Aperture = item.Aperture;
 0826            dto.ShutterSpeed = item.ShutterSpeed;
 827
 0828            dto.Latitude = item.Latitude;
 0829            dto.Longitude = item.Longitude;
 0830            dto.Altitude = item.Altitude;
 0831            dto.IsoSpeedRating = item.IsoSpeedRating;
 832
 0833            var album = item.AlbumEntity;
 834
 0835            if (album is not null)
 836            {
 0837                dto.Album = album.Name;
 0838                dto.AlbumId = album.Id;
 839            }
 0840        }
 841
 842        private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
 843        {
 0844            if (!string.IsNullOrEmpty(item.Album))
 845            {
 0846                var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
 0847                {
 0848                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
 0849                    Name = item.Album,
 0850                    Limit = 1
 0851                });
 852
 0853                if (parentAlbumIds.Count > 0)
 854                {
 0855                    dto.AlbumId = parentAlbumIds[0];
 856                }
 857            }
 858
 0859            dto.Album = item.Album;
 0860        }
 861
 862        private string[] GetImageTags(BaseItem item, List<ItemImageInfo> images)
 863        {
 21864            return images
 21865                .Select(p => GetImageCacheTag(item, p))
 21866                .Where(i => i is not null)
 21867                .ToArray()!; // null values got filtered out
 868        }
 869
 870        private string? GetImageCacheTag(BaseItem item, ItemImageInfo image)
 871        {
 872            try
 873            {
 11874                return _imageProcessor.GetImageCacheTag(item, image);
 875            }
 0876            catch (Exception ex)
 877            {
 0878                _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
 0879                return null;
 880            }
 11881        }
 882
 883        /// <summary>
 884        /// Attaches People DTO's to a DTOBaseItem.
 885        /// </summary>
 886        /// <param name="dto">The dto.</param>
 887        /// <param name="item">The item.</param>
 888        /// <param name="user">The requesting user.</param>
 889        /// <param name="prefetchedPeople">People fetched in batch by the caller; when null the people are queried per i
 890        private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null, IReadOnlyList<PersonInfo>? prefetch
 891        {
 892            // When rendering a page of items the caller batch-fetches people for every item up
 893            // front and passes them in, avoiding one GetPeople query per item. Fall back to the
 894            // per-item query for the single item path where no batch is available.
 8895            var source = prefetchedPeople ?? _libraryManager.GetPeople(item);
 896
 897            // Ordering by person type to ensure actors and artists are at the front.
 898            // This is taking advantage of the fact that they both begin with A
 899            // This should be improved in the future
 8900            var people = source.OrderBy(i => i.SortOrder ?? int.MaxValue)
 8901                .ThenBy(i =>
 8902                {
 8903                    if (i.IsType(PersonKind.Actor))
 8904                    {
 8905                        return 0;
 8906                    }
 8907
 8908                    if (i.IsType(PersonKind.GuestStar))
 8909                    {
 8910                        return 1;
 8911                    }
 8912
 8913                    if (i.IsType(PersonKind.Director))
 8914                    {
 8915                        return 2;
 8916                    }
 8917
 8918                    if (i.IsType(PersonKind.Writer))
 8919                    {
 8920                        return 3;
 8921                    }
 8922
 8923                    if (i.IsType(PersonKind.Producer))
 8924                    {
 8925                        return 4;
 8926                    }
 8927
 8928                    if (i.IsType(PersonKind.Composer))
 8929                    {
 8930                        return 4;
 8931                    }
 8932
 8933                    return 10;
 8934                })
 8935                .ToList();
 936
 8937            var list = new List<BaseItemPerson>();
 938
 8939            Dictionary<string, Person> dictionary = people.Select(p => p.Name)
 8940                .Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
 8941                {
 8942                    try
 8943                    {
 8944                        return _libraryManager.GetPerson(c);
 8945                    }
 8946                    catch (Exception ex)
 8947                    {
 8948                        _logger.LogError(ex, "Error getting person {Name}", c);
 8949                        return null;
 8950                    }
 8951                }).Where(i => i is not null)
 8952                .Where(i => user is null || i!.IsVisible(user))
 8953                .DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase)
 8954                .ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out
 955
 20956            for (var i = 0; i < people.Count; i++)
 957            {
 2958                var person = people[i];
 959
 2960                var baseItemPerson = new BaseItemPerson
 2961                {
 2962                    Name = person.Name,
 2963                    Role = person.Role,
 2964                    Type = person.Type
 2965                };
 966
 2967                if (dictionary.TryGetValue(person.Name, out Person? entity))
 968                {
 2969                    baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
 2970                    baseItemPerson.Id = entity.Id;
 2971                    if (dto.ImageBlurHashes is not null)
 972                    {
 973                        // Only add BlurHash for the person's image.
 0974                        baseItemPerson.ImageBlurHashes = [];
 0975                        foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
 976                        {
 0977                            if (blurHash is not null)
 978                            {
 0979                                baseItemPerson.ImageBlurHashes[imageType] = [];
 0980                                foreach (var (imageId, blurHashValue) in blurHash)
 981                                {
 0982                                    if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalI
 983                                    {
 0984                                        baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
 985                                    }
 986                                }
 987                            }
 988                        }
 989                    }
 990
 2991                    list.Add(baseItemPerson);
 992                }
 993            }
 994
 8995            dto.People = list.ToArray();
 8996        }
 997
 998        /// <summary>
 999        /// Attaches the studios.
 1000        /// </summary>
 1001        /// <param name="dto">The dto.</param>
 1002        /// <param name="item">The item.</param>
 1003        private void AttachStudios(BaseItemDto dto, BaseItem item)
 1004        {
 61005            dto.Studios = item.Studios
 61006                .Where(i => !string.IsNullOrEmpty(i))
 61007                .Select(i => new NameGuidPair
 61008                {
 61009                    Name = i,
 61010                    Id = _libraryManager.GetStudioId(i)
 61011                })
 61012                .ToArray();
 61013        }
 1014
 1015        private void AttachGenreItems(BaseItemDto dto, BaseItem item)
 1016        {
 61017            dto.GenreItems = item.Genres
 61018                .Where(i => !string.IsNullOrEmpty(i))
 61019                .Select(i => new NameGuidPair
 61020                {
 61021                    Name = i,
 61022                    Id = GetGenreId(i, item)
 61023                })
 61024                .ToArray();
 61025        }
 1026
 1027        private Guid GetGenreId(string name, BaseItem owner)
 1028        {
 01029            if (owner is IHasMusicGenres)
 1030            {
 01031                return _libraryManager.GetMusicGenreId(name);
 1032            }
 1033
 01034            return _libraryManager.GetGenreId(name);
 1035        }
 1036
 1037        private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
 1038        {
 81039            var image = item.GetImageInfo(imageType, imageIndex);
 81040            if (image is not null)
 1041            {
 31042                return GetTagAndFillBlurhash(dto, item, image);
 1043            }
 1044
 51045            return null;
 1046        }
 1047
 1048        private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
 1049        {
 111050            var tag = GetImageCacheTag(item, image);
 111051            if (tag is null)
 1052            {
 01053                return null;
 1054            }
 1055
 111056            if (!string.IsNullOrEmpty(image.BlurHash))
 1057            {
 01058                dto.ImageBlurHashes ??= [];
 1059
 01060                if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
 1061                {
 01062                    value = [];
 01063                    dto.ImageBlurHashes[image.Type] = value;
 1064                }
 1065
 01066                value[tag] = image.BlurHash;
 1067            }
 1068
 111069            return tag;
 1070        }
 1071
 1072        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit)
 1073        {
 211074            return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList());
 1075        }
 1076
 1077        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInf
 1078        {
 211079            var tags = GetImageTags(item, images);
 211080            var hashes = new Dictionary<string, string>();
 421081            for (int i = 0; i < images.Count; i++)
 1082            {
 01083                var img = images[i];
 01084                if (!string.IsNullOrEmpty(img.BlurHash))
 1085                {
 01086                    var tag = tags[i];
 01087                    hashes[tag] = img.BlurHash;
 1088                }
 1089            }
 1090
 211091            if (hashes.Count > 0)
 1092            {
 01093                dto.ImageBlurHashes ??= [];
 1094
 01095                dto.ImageBlurHashes[imageType] = hashes;
 1096            }
 1097
 211098            return tags;
 1099        }
 1100
 1101        /// <summary>
 1102        /// Sets simple property values on a DTOBaseItem.
 1103        /// </summary>
 1104        /// <param name="dto">The dto.</param>
 1105        /// <param name="item">The item.</param>
 1106        /// <param name="owner">The owner.</param>
 1107        /// <param name="options">The options.</param>
 1108        /// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
 1109        /// <param name="user">The user, for per-user values such as the accessible media source count.</param>
 1110        /// <param name="alternateVersionItemIds">Optional pre-fetched set of item IDs that own alternate versions, shar
 1111        private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDic
 1112        {
 261113            if (options.ContainsField(ItemFields.DateCreated))
 1114            {
 61115                dto.DateCreated = item.DateCreated;
 1116            }
 1117
 261118            if (options.ContainsField(ItemFields.Settings))
 1119            {
 61120                dto.LockedFields = item.LockedFields;
 61121                dto.LockData = item.IsLocked;
 61122                dto.ForcedSortName = item.ForcedSortName;
 1123            }
 1124
 261125            dto.Container = item.Container;
 261126            dto.EndDate = item.EndDate;
 1127
 261128            if (options.ContainsField(ItemFields.ExternalUrls))
 1129            {
 61130                dto.ExternalUrls = _providerManager.GetExternalUrls(item).ToArray();
 1131            }
 1132
 261133            if (options.ContainsField(ItemFields.Tags))
 1134            {
 61135                dto.Tags = item.Tags;
 1136            }
 1137
 261138            if (item is IHasAspectRatio hasAspectRatio)
 1139            {
 61140                dto.AspectRatio = hasAspectRatio.AspectRatio;
 1141            }
 1142
 261143            dto.ImageBlurHashes = [];
 1144
 261145            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
 261146            if (backdropLimit > 0)
 1147            {
 211148                dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
 1149            }
 1150
 261151            if (options.ContainsField(ItemFields.Genres))
 1152            {
 61153                dto.Genres = item.Genres;
 61154                AttachGenreItems(dto, item);
 1155            }
 1156
 261157            if (options.EnableImages)
 1158            {
 211159                dto.ImageTags = [];
 1160
 1161                // Prevent implicitly captured closure
 211162                var currentItem = item;
 521163                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)))
 1164                {
 51165                    if (options.GetImageLimit(image.Type) > 0)
 1166                    {
 51167                        var tag = GetTagAndFillBlurhash(dto, item, image);
 1168
 51169                        if (tag is not null)
 1170                        {
 51171                            dto.ImageTags[image.Type] = tag;
 1172                        }
 1173                    }
 1174                }
 1175            }
 1176
 261177            dto.Id = item.Id;
 261178            dto.IndexNumber = item.IndexNumber;
 261179            dto.ParentIndexNumber = item.ParentIndexNumber;
 1180
 261181            if (item.IsFolder)
 1182            {
 201183                dto.IsFolder = true;
 1184            }
 61185            else if (item is IHasMediaSources)
 1186            {
 61187                dto.IsFolder = false;
 1188            }
 1189
 261190            dto.MediaType = item.MediaType;
 1191
 261192            if (item is not LiveTvProgram)
 1193            {
 261194                dto.LocationType = item.LocationType;
 1195            }
 1196
 261197            dto.Audio = item.Audio;
 1198
 261199            if (options.ContainsField(ItemFields.Settings))
 1200            {
 61201                dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 61202                dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 1203            }
 1204
 261205            dto.CriticRating = item.CriticRating;
 1206
 261207            if (item is IHasDisplayOrder hasDisplayOrder)
 1208            {
 01209                dto.DisplayOrder = hasDisplayOrder.DisplayOrder;
 1210            }
 1211
 261212            if (item is IHasCollectionType hasCollectionType)
 1213            {
 71214                dto.CollectionType = hasCollectionType.CollectionType;
 1215            }
 1216
 261217            if (options.ContainsField(ItemFields.RemoteTrailers))
 1218            {
 61219                dto.RemoteTrailers = item.RemoteTrailers;
 1220            }
 1221
 261222            dto.Name = item.Name;
 261223            dto.OfficialRating = item.OfficialRating;
 1224
 261225            if (options.ContainsField(ItemFields.Overview))
 1226            {
 61227                dto.Overview = item.Overview;
 1228            }
 1229
 261230            if (options.ContainsField(ItemFields.OriginalTitle))
 1231            {
 61232                dto.OriginalTitle = item.OriginalTitle;
 1233            }
 1234
 261235            dto.OriginalLanguage = item.OriginalLanguage;
 1236
 261237            if (options.ContainsField(ItemFields.ParentId))
 1238            {
 61239                dto.ParentId = item.DisplayParentId;
 1240            }
 1241
 261242            AddInheritedImages(dto, item, options, owner, artistsBatch);
 1243
 261244            if (options.ContainsField(ItemFields.Path))
 1245            {
 61246                dto.Path = GetMappedPath(item, owner);
 1247            }
 1248
 261249            if (options.ContainsField(ItemFields.EnableMediaSourceDisplay))
 1250            {
 61251                dto.EnableMediaSourceDisplay = item.EnableMediaSourceDisplay;
 1252            }
 1253
 261254            dto.PremiereDate = item.PremiereDate;
 261255            dto.ProductionYear = item.ProductionYear;
 1256
 261257            if (options.ContainsField(ItemFields.ProviderIds))
 1258            {
 61259                dto.ProviderIds = item.ProviderIds;
 1260            }
 1261
 261262            dto.RunTimeTicks = item.RunTimeTicks;
 1263
 261264            if (options.ContainsField(ItemFields.SortName))
 1265            {
 61266                dto.SortName = item.SortName;
 1267            }
 1268
 261269            if (options.ContainsField(ItemFields.CustomRating))
 1270            {
 61271                dto.CustomRating = item.CustomRating;
 1272            }
 1273
 261274            if (options.ContainsField(ItemFields.Taglines))
 1275            {
 61276                if (!string.IsNullOrEmpty(item.Tagline))
 1277                {
 01278                    dto.Taglines = new string[] { item.Tagline };
 1279                }
 1280
 61281                dto.Taglines ??= Array.Empty<string>();
 1282            }
 1283
 261284            dto.Type = item.GetBaseItemKind();
 261285            if ((item.CommunityRating ?? 0) > 0)
 1286            {
 01287                dto.CommunityRating = item.CommunityRating;
 1288            }
 1289
 261290            if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder)
 1291            {
 01292                dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
 1293            }
 1294
 261295            if (item.LUFS.HasValue)
 1296            {
 1297                // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
 01298                dto.NormalizationGain = -18f - item.LUFS;
 1299            }
 261300            else if (item.NormalizationGain.HasValue)
 1301            {
 01302                dto.NormalizationGain = item.NormalizationGain;
 1303            }
 1304
 1305            // Add audio info
 261306            if (item is Audio audio)
 1307            {
 01308                dto.Album = audio.Album;
 01309                dto.ExtraType = audio.ExtraType;
 1310
 01311                var albumParent = audio.AlbumEntity;
 1312
 01313                if (albumParent is not null)
 1314                {
 01315                    dto.AlbumId = albumParent.Id;
 01316                    dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
 01317                    if (albumParent.LUFS.HasValue)
 1318                    {
 1319                        // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
 01320                        dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
 1321                    }
 01322                    else if (albumParent.NormalizationGain.HasValue)
 1323                    {
 01324                        dto.AlbumNormalizationGain = albumParent.NormalizationGain;
 1325                    }
 1326                }
 1327
 1328                // if (options.ContainsField(ItemFields.MediaSourceCount))
 1329                // {
 1330                // Songs always have one
 1331                // }
 1332            }
 1333
 261334            if (item is IHasArtist hasArtist)
 1335            {
 41336                dto.Artists = hasArtist.Artists;
 1337
 1338                // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
 1339                // {
 1340                //    EnableTotalRecordCount = false,
 1341                //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
 1342                // });
 1343
 1344                // dto.ArtistItems = artistItems.Items
 1345                //    .Select(i =>
 1346                //    {
 1347                //        var artist = i.Item1;
 1348                //        return new NameIdPair
 1349                //        {
 1350                //            Name = artist.Name,
 1351                //            Id = artist.Id.ToString("N", CultureInfo.InvariantCulture)
 1352                //        };
 1353                //    })
 1354                //    .ToList();
 1355
 1356                // Include artists that are not in the database yet, e.g., just added via metadata editor
 1357                // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
 41358                var artistsLookup = artistsBatch
 41359                    ?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
 1360
 41361                dto.ArtistItems = hasArtist.Artists
 41362                    .Where(name => !string.IsNullOrWhiteSpace(name))
 41363                    .Distinct()
 41364                    .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
 41365                        ? new NameGuidPair { Name = name, Id = artists[0].Id }
 41366                        : null)
 41367                    .Where(item => item is not null)
 41368                    .ToArray();
 1369            }
 1370
 261371            if (item is IHasAlbumArtist hasAlbumArtist)
 1372            {
 41373                dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
 1374
 1375                // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
 1376                // {
 1377                //    EnableTotalRecordCount = false,
 1378                //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
 1379                // });
 1380
 1381                // dto.AlbumArtists = artistItems.Items
 1382                //    .Select(i =>
 1383                //    {
 1384                //        var artist = i.Item1;
 1385                //        return new NameIdPair
 1386                //        {
 1387                //            Name = artist.Name,
 1388                //            Id = artist.Id.ToString("N", CultureInfo.InvariantCulture)
 1389                //        };
 1390                //    })
 1391                //    .ToList();
 1392
 41393                var albumArtistsLookup = artistsBatch
 41394                    ?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(
 1395
 41396                dto.AlbumArtists = hasAlbumArtist.AlbumArtists
 41397                    .Where(name => !string.IsNullOrWhiteSpace(name))
 41398                    .Distinct()
 41399                    .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 
 41400                        ? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
 41401                        : null)
 41402                    .Where(item => item is not null)
 41403                    .ToArray();
 1404            }
 1405
 1406            // Add video info
 261407            if (item is Video video)
 1408            {
 61409                dto.VideoType = video.VideoType;
 61410                dto.Video3DFormat = video.Video3DFormat;
 61411                dto.IsoType = video.IsoType;
 1412
 61413                if (video.HasSubtitles)
 1414                {
 01415                    dto.HasSubtitles = video.HasSubtitles;
 1416                }
 1417
 61418                if (video.AdditionalParts.Length != 0)
 1419                {
 01420                    dto.PartCount = video.AdditionalParts.Length + 1;
 1421                }
 1422
 61423                if (options.ContainsField(ItemFields.MediaSourceCount))
 1424                {
 1425                    // A video with no primary version and no alternate versions always has a single
 1426                    // media source. Only compute the count for videos that might have more: a primary
 1427                    // version, or membership in the batch's set of items that own alternate versions.
 1428                    // Without the batch we can't rule it out, so fall back to computing (the single-item
 1429                    // path). Everything else is the common case and keeps the default count of one.
 31430                    var mayHaveAlternateVersions = alternateVersionItemIds is null
 31431                        || video.PrimaryVersionId.HasValue
 31432                        || alternateVersionItemIds.Contains(video.Id);
 1433
 31434                    if (mayHaveAlternateVersions)
 1435                    {
 1436                        // Match the per-user filtering of the media sources: versions the user cannot
 1437                        // access are not selectable, so they must not count towards the badge either.
 11438                        var mediaSourceCount = user is null
 11439                            || (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.
 11440                                ? video.MediaSourceCount
 11441                                : video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user)
 11442                        if (mediaSourceCount != 1)
 1443                        {
 11444                            dto.MediaSourceCount = mediaSourceCount;
 1445                        }
 1446                    }
 1447                }
 1448
 61449                if (options.ContainsField(ItemFields.Trickplay))
 1450                {
 01451                    var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
 01452                    dto.Trickplay = trickplay.ToDictionary(
 01453                        mediaStream => mediaStream.Key,
 01454                        mediaStream => mediaStream.Value.ToDictionary(
 01455                            width => width.Key,
 01456                            width => new TrickplayInfoDto(width.Value)));
 1457                }
 1458
 61459                dto.ExtraType = video.ExtraType;
 1460            }
 1461
 261462            if (options.ContainsField(ItemFields.Chapters))
 1463            {
 61464                dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
 1465            }
 1466
 261467            if (options.ContainsField(ItemFields.MediaStreams))
 1468            {
 1469                // Add VideoInfo
 61470                if (item is IHasMediaSources)
 1471                {
 1472                    MediaStream[] mediaStreams;
 1473
 01474                    if (dto.MediaSources is not null && dto.MediaSources.Length > 0)
 1475                    {
 01476                        if (item.SourceType == SourceType.Channel)
 1477                        {
 01478                            mediaStreams = dto.MediaSources[0].MediaStreams.ToArray();
 1479                        }
 1480                        else
 1481                        {
 01482                            string id = item.Id.ToString("N", CultureInfo.InvariantCulture);
 01483                            mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalI
 01484                                .SelectMany(i => i.MediaStreams)
 01485                                .ToArray();
 1486                        }
 1487                    }
 1488                    else
 1489                    {
 01490                        mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
 1491                    }
 1492
 01493                    dto.MediaStreams = mediaStreams;
 1494                }
 1495            }
 1496
 261497            BaseItem[]? allExtras = null;
 1498
 261499            if (options.ContainsField(ItemFields.SpecialFeatureCount))
 1500            {
 61501                allExtras = item.GetExtras().ToArray();
 61502                dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contai
 1503            }
 1504
 261505            if (options.ContainsField(ItemFields.LocalTrailerCount))
 1506            {
 61507                if (item is IHasTrailers hasTrailers)
 1508                {
 01509                    dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
 1510                }
 1511                else
 1512                {
 61513                    dto.LocalTrailerCount = (allExtras ?? item.GetExtras()).Count(i => i.ExtraType == ExtraType.Trailer)
 1514                }
 1515            }
 1516
 1517            // Add EpisodeInfo
 261518            if (item is Episode episode)
 1519            {
 31520                dto.IndexNumberEnd = episode.IndexNumberEnd;
 31521                dto.SeriesName = episode.SeriesName;
 1522
 31523                if (options.ContainsField(ItemFields.SpecialEpisodeNumbers))
 1524                {
 01525                    dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber;
 01526                    dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber;
 01527                    dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber;
 1528                }
 1529
 31530                dto.SeasonName = episode.SeasonName;
 31531                dto.SeasonId = episode.SeasonId;
 31532                dto.SeriesId = episode.SeriesId;
 1533
 31534                Series? episodeSeries = null;
 1535
 1536                // this block will add the series poster for episodes without a poster
 1537                // TODO maybe remove the if statement entirely
 1538                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
 1539                {
 31540                    episodeSeries ??= episode.Series;
 31541                    if (episodeSeries is not null)
 1542                    {
 31543                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
 31544                        if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
 1545                        {
 01546                            AttachPrimaryImageAspectRatio(dto, episodeSeries);
 1547                        }
 1548                    }
 1549                }
 1550
 31551                if (options.GetImageLimit(ImageType.Primary) > 0)
 1552                {
 31553                    var episodeSeason = episode.Season;
 31554                    var seasonPrimaryTag = episodeSeason is not null
 31555                        ? GetTagAndFillBlurhash(dto, episodeSeason, ImageType.Primary)
 31556                        : null;
 1557
 31558                    if (seasonPrimaryTag is not null)
 1559                    {
 11560                        dto.ParentPrimaryImageItemId = episodeSeason!.Id;
 11561                        dto.ParentPrimaryImageTag = seasonPrimaryTag;
 1562                    }
 21563                    else if (episodeSeries is not null && dto.SeriesPrimaryImageTag is not null)
 1564                    {
 11565                        dto.ParentPrimaryImageItemId = episodeSeries.Id;
 11566                        dto.ParentPrimaryImageTag = dto.SeriesPrimaryImageTag;
 1567                    }
 1568                }
 1569
 31570                if (options.ContainsField(ItemFields.SeriesStudio))
 1571                {
 01572                    episodeSeries ??= episode.Series;
 01573                    if (episodeSeries is not null)
 1574                    {
 01575                        dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
 1576                    }
 1577                }
 1578            }
 1579
 1580            // Add SeriesInfo
 1581            Series? series;
 261582            if (item is Series tmp)
 1583            {
 01584                series = tmp;
 01585                dto.AirDays = series.AirDays;
 01586                dto.AirTime = series.AirTime;
 01587                dto.Status = series.Status?.ToString();
 1588            }
 1589
 1590            // Add SeasonInfo
 261591            if (item is Season season)
 1592            {
 31593                dto.SeriesName = season.SeriesName;
 31594                dto.SeriesId = season.SeriesId;
 1595
 31596                series = null;
 1597
 31598                if (options.ContainsField(ItemFields.SeriesStudio))
 1599                {
 01600                    series ??= season.Series;
 01601                    if (series is not null)
 1602                    {
 01603                        dto.SeriesStudio = series.Studios.FirstOrDefault();
 1604                    }
 1605                }
 1606
 1607                // this block will add the series poster for seasons without a poster
 1608                // TODO maybe remove the if statement entirely
 1609                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
 1610                {
 31611                    series ??= season.Series;
 31612                    if (series is not null)
 1613                    {
 01614                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
 01615                        if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
 1616                        {
 01617                            AttachPrimaryImageAspectRatio(dto, series);
 1618                        }
 1619                    }
 1620                }
 1621            }
 1622
 261623            if (item is MusicVideo musicVideo)
 1624            {
 01625                SetMusicVideoProperties(dto, musicVideo);
 1626            }
 1627
 261628            if (item is Book book)
 1629            {
 01630                SetBookProperties(dto, book);
 1631            }
 1632
 261633            if (options.ContainsField(ItemFields.ProductionLocations))
 1634            {
 61635                if (item.ProductionLocations.Length > 0 || item is Movie)
 1636                {
 01637                    dto.ProductionLocations = item.ProductionLocations;
 1638                }
 1639            }
 1640
 261641            if (options.ContainsField(ItemFields.Width))
 1642            {
 61643                var width = item.Width;
 61644                if (width > 0)
 1645                {
 01646                    dto.Width = width;
 1647                }
 1648            }
 1649
 261650            if (options.ContainsField(ItemFields.Height))
 1651            {
 61652                var height = item.Height;
 61653                if (height > 0)
 1654                {
 01655                    dto.Height = height;
 1656                }
 1657            }
 1658
 261659            if (options.ContainsField(ItemFields.IsHD))
 1660            {
 1661                // Compatibility
 61662                if (item.IsHD)
 1663                {
 01664                    dto.IsHD = true;
 1665                }
 1666            }
 1667
 261668            if (item is Photo photo)
 1669            {
 01670                SetPhotoProperties(dto, photo);
 1671            }
 1672
 261673            dto.ChannelId = item.ChannelId;
 1674
 261675            if (item.SourceType == SourceType.Channel)
 1676            {
 01677                var channel = _libraryManager.GetItemById(item.ChannelId);
 01678                if (channel is not null)
 1679                {
 01680                    dto.ChannelName = channel.Name;
 1681                }
 1682            }
 261683        }
 1684
 1685        private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem, IReadOnlyDictionary<string,
 1686        {
 131687            if (currentItem is MusicAlbum musicAlbum)
 1688            {
 41689                var artist = GetBatchedAlbumArtist(musicAlbum, artistsBatch) ?? musicAlbum.GetMusicArtist(new DtoOptions
 41690                if (artist is not null)
 1691                {
 21692                    return artist;
 1693                }
 1694            }
 1695
 111696            var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
 1697
 111698            if (parent is null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is 
 1699            {
 51700                parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
 1701            }
 1702
 111703            return parent;
 1704        }
 1705
 1706        private static MusicArtist? GetBatchedAlbumArtist(MusicAlbum album, IReadOnlyDictionary<string, MusicArtist[]>? 
 1707        {
 41708            if (artistsBatch is null)
 1709            {
 21710                return null;
 1711            }
 1712
 21713            var name = album.AlbumArtists.Count > 0 ? album.AlbumArtists[0] : null;
 21714            return !string.IsNullOrEmpty(name) && artistsBatch.TryGetValue(name, out var artists) && artists.Length > 0
 21715                ? artists[0]
 21716                : null;
 1717        }
 1718
 1719        private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner, IReadOnlyDi
 1720        {
 261721            if (item is UserView { ViewType: CollectionType.playlists } playlistsView
 261722                && options.GetImageLimit(ImageType.Primary) > 0
 261723                && !playlistsView.DisplayParentId.IsEmpty())
 1724            {
 21725                var displayParent = _libraryManager.GetItemById(playlistsView.DisplayParentId);
 21726                var displayParentPrimaryImage = displayParent?.GetImageInfo(ImageType.Primary, 0);
 1727
 21728                if (displayParentPrimaryImage is not null)
 1729                {
 11730                    dto.ImageTags?.Remove(ImageType.Primary);
 11731                    dto.ParentPrimaryImageItemId = displayParent!.Id;
 11732                    dto.ParentPrimaryImageTag = GetTagAndFillBlurhash(dto, displayParent, displayParentPrimaryImage);
 1733                }
 1734            }
 1735
 261736            if (!item.SupportsInheritedParentImages)
 1737            {
 131738                return;
 1739            }
 1740
 131741            var logoLimit = options.GetImageLimit(ImageType.Logo);
 131742            var artLimit = options.GetImageLimit(ImageType.Art);
 131743            var thumbLimit = options.GetImageLimit(ImageType.Thumb);
 131744            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
 1745
 1746            // For now. Emby apps are not using this
 131747            artLimit = 0;
 1748
 131749            if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0)
 1750            {
 31751                return;
 1752            }
 1753
 101754            BaseItem? parent = null;
 101755            var isFirst = true;
 1756
 101757            var imageTags = dto.ImageTags;
 1758
 131759            while ((!(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
 131760                || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
 131761                || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
 131762                || parent is Series)
 1763            {
 131764                parent ??= isFirst ? GetImageDisplayParent(item, item, artistsBatch) ?? owner : parent;
 131765                if (parent is null)
 1766                {
 1767                    break;
 1768                }
 1769
 81770                var allImages = parent.ImageInfos;
 1771
 81772                if (logoLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogo
 1773                {
 81774                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
 1775
 81776                    if (image is not null)
 1777                    {
 01778                        dto.ParentLogoItemId = parent.Id;
 01779                        dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1780                    }
 1781                }
 1782
 81783                if (artLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtIte
 1784                {
 01785                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
 1786
 01787                    if (image is not null)
 1788                    {
 01789                        dto.ParentArtItemId = parent.Id;
 01790                        dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1791                    }
 1792                }
 1793
 81794                if (thumbLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentT
 1795                {
 81796                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
 1797
 81798                    if (image is not null)
 1799                    {
 21800                        dto.ParentThumbItemId = parent.Id;
 21801                        dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1802                    }
 1803                }
 1804
 81805                if (backdropLimit > 0 && !((dto.BackdropImageTags is not null && dto.BackdropImageTags.Length > 0) || (d
 1806                {
 81807                    var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
 1808
 81809                    if (images.Count > 0)
 1810                    {
 01811                        dto.ParentBackdropItemId = parent.Id;
 01812                        dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
 1813                    }
 1814                }
 1815
 81816                isFirst = false;
 1817
 81818                if (!parent.SupportsInheritedParentImages)
 1819                {
 1820                    break;
 1821                }
 1822
 31823                parent = GetImageDisplayParent(parent, item, artistsBatch);
 1824            }
 101825        }
 1826
 1827        private string GetMappedPath(BaseItem item, BaseItem? ownerItem)
 1828        {
 61829            var path = item.Path;
 1830
 61831            if (item.IsFileProtocol)
 1832            {
 61833                path = _libraryManager.GetPathAfterNetworkSubstitution(path, ownerItem ?? item);
 1834            }
 1835
 61836            return path;
 1837        }
 1838
 1839        /// <summary>
 1840        /// Attaches the primary image aspect ratio.
 1841        /// </summary>
 1842        /// <param name="dto">The dto.</param>
 1843        /// <param name="item">The item.</param>
 1844        public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
 1845        {
 71846            dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
 71847        }
 1848
 1849        public double? GetPrimaryImageAspectRatio(BaseItem item)
 1850        {
 71851            var imageInfo = item.GetImageInfo(ImageType.Primary, 0);
 1852
 71853            if (imageInfo is null)
 1854            {
 61855                return null;
 1856            }
 1857
 11858            if (!imageInfo.IsLocalFile)
 1859            {
 11860                return item.GetDefaultPrimaryImageAspectRatio();
 1861            }
 1862
 1863            try
 1864            {
 01865                var size = _imageProcessor.GetImageDimensions(item, imageInfo);
 01866                var width = size.Width;
 01867                var height = size.Height;
 01868                if (width > 0 && height > 0)
 1869                {
 01870                    return (double)width / height;
 1871                }
 01872            }
 01873            catch (Exception ex)
 1874            {
 01875                _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
 01876            }
 1877
 01878            return item.GetDefaultPrimaryImageAspectRatio();
 01879        }
 1880    }
 1881}

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[]>,MediaBrowser.Controller.Library.VersionResumeData,System.Collections.Generic.IReadOnlyDictionary`2<System.Guid,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.PersonInfo>>,System.Collections.Generic.IReadOnlySet`1<System.Guid>)
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)
GetItemCountsBatch(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User)
SetItemByNameInfo(MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IReadOnlyDictionary`2<System.Guid,MediaBrowser.Model.Dto.ItemCounts>)
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>>,MediaBrowser.Controller.Library.VersionResumeData)
GetUserItemDataDto(MediaBrowser.Controller.Entities.UserItemData,System.Guid)
GetChildCountBatch(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User)
GetChildCountSourceIds(MediaBrowser.Controller.Entities.Folder,Jellyfin.Database.Implementations.Entities.User)
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,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.PersonInfo>)
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[]>,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IReadOnlySet`1<System.Guid>)
GetImageDisplayParent(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IReadOnlyDictionary`2<System.String,MediaBrowser.Controller.Entities.Audio.MusicArtist[]>)
GetBatchedAlbumArtist(MediaBrowser.Controller.Entities.Audio.MusicAlbum,System.Collections.Generic.IReadOnlyDictionary`2<System.String,MediaBrowser.Controller.Entities.Audio.MusicArtist[]>)
AddInheritedImages(MediaBrowser.Model.Dto.BaseItemDto,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Dto.DtoOptions,MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IReadOnlyDictionary`2<System.String,MediaBrowser.Controller.Entities.Audio.MusicArtist[]>)
GetMappedPath(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
AttachPrimaryImageAspectRatio(MediaBrowser.Model.Dto.IItemDto,MediaBrowser.Controller.Entities.BaseItem)
GetPrimaryImageAspectRatio(MediaBrowser.Controller.Entities.BaseItem)