< 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
49%
Covered lines: 353
Uncovered lines: 361
Coverable lines: 714
Total lines: 1520
Line coverage: 49.4%
Branch coverage
34%
Covered branches: 167
Total branches: 479
Branch coverage: 34.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/27/2025 - 12:11:51 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: 1520 12/27/2025 - 12:11:51 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: 1520

Coverage delta

Coverage delta 2 -2

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(IReadOnlyList<BaseItem> items, DtoOptions options, User? user 
 157        {
 4158            var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
 4159            var returnItems = new BaseItemDto[accessibleItems.Count];
 4160            List<(BaseItem, BaseItemDto)>? programTuples = null;
 4161            List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
 162
 14163            for (int index = 0; index < accessibleItems.Count; index++)
 164            {
 3165                var item = accessibleItems[index];
 3166                var dto = GetBaseItemDtoInternal(item, options, user, owner);
 167
 3168                if (item is LiveTvChannel tvChannel)
 169                {
 0170                    (channelTuples ??= []).Add((dto, tvChannel));
 171                }
 3172                else if (item is LiveTvProgram)
 173                {
 0174                    (programTuples ??= []).Add((item, dto));
 175                }
 176
 3177                if (options.ContainsField(ItemFields.ItemCounts))
 178                {
 0179                    SetItemByNameInfo(dto, user);
 180                }
 181
 3182                returnItems[index] = dto;
 183            }
 184
 4185            if (programTuples is not null)
 186            {
 0187                LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
 188            }
 189
 4190            if (channelTuples is not null)
 191            {
 0192                LivetvManager.AddChannelInfo(channelTuples, options, user);
 193            }
 194
 4195            return returnItems;
 196        }
 197
 198        public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
 199        {
 6200            var dto = GetBaseItemDtoInternal(item, options, user, owner);
 6201            if (item is LiveTvChannel tvChannel)
 202            {
 0203                LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
 204            }
 6205            else if (item is LiveTvProgram)
 206            {
 0207                LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
 208            }
 209
 6210            if (options.ContainsField(ItemFields.ItemCounts))
 211            {
 6212                SetItemByNameInfo(dto, user);
 213            }
 214
 6215            return dto;
 216        }
 217
 218        private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner
 219        {
 9220            var dto = new BaseItemDto
 9221            {
 9222                ServerId = _appHost.SystemId
 9223            };
 224
 9225            if (item.SourceType == SourceType.Channel)
 226            {
 0227                dto.SourceType = item.SourceType.ToString();
 228            }
 229
 9230            if (options.ContainsField(ItemFields.People))
 231            {
 6232                AttachPeople(dto, item, user);
 233            }
 234
 9235            if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
 236            {
 237                try
 238                {
 6239                    AttachPrimaryImageAspectRatio(dto, item);
 6240                }
 0241                catch (Exception ex)
 242                {
 243                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 0244                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
 0245                }
 246            }
 247
 9248            if (options.ContainsField(ItemFields.DisplayPreferencesId))
 249            {
 6250                dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture);
 251            }
 252
 9253            if (user is not null)
 254            {
 9255                AttachUserSpecificInfo(dto, item, user, options);
 256            }
 257
 9258            if (item is IHasMediaSources
 9259                && options.ContainsField(ItemFields.MediaSources))
 260            {
 0261                dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray();
 262
 0263                NormalizeMediaSourceContainers(dto);
 264            }
 265
 9266            if (options.ContainsField(ItemFields.Studios))
 267            {
 6268                AttachStudios(dto, item);
 269            }
 270
 9271            AttachBasicFields(dto, item, owner, options);
 272
 9273            if (options.ContainsField(ItemFields.CanDelete))
 274            {
 6275                dto.CanDelete = user is null
 6276                    ? item.CanDelete()
 6277                    : item.CanDelete(user);
 278            }
 279
 9280            if (options.ContainsField(ItemFields.CanDownload))
 281            {
 6282                dto.CanDownload = user is null
 6283                    ? item.CanDownload()
 6284                    : item.CanDownload(user);
 285            }
 286
 9287            if (options.ContainsField(ItemFields.Etag))
 288            {
 6289                dto.Etag = item.GetEtag(user);
 290            }
 291
 9292            var activeRecording = _recordingsManager.GetActiveRecordingInfo(item.Path);
 9293            if (activeRecording is not null)
 294            {
 0295                dto.Type = BaseItemKind.Recording;
 0296                dto.CanDownload = false;
 0297                dto.RunTimeTicks = null;
 298
 0299                if (!string.IsNullOrEmpty(dto.SeriesName))
 300                {
 0301                    dto.EpisodeTitle = dto.Name;
 0302                    dto.Name = dto.SeriesName;
 303                }
 304
 0305                LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
 306            }
 307
 9308            if (item is Audio audio)
 309            {
 0310                dto.HasLyrics = audio.GetMediaStreams().Any(s => s.Type == MediaStreamType.Lyric);
 311            }
 312
 9313            return dto;
 314        }
 315
 316        private static void NormalizeMediaSourceContainers(BaseItemDto dto)
 317        {
 0318            foreach (var mediaSource in dto.MediaSources)
 319            {
 0320                var container = mediaSource.Container;
 0321                if (string.IsNullOrEmpty(container))
 322                {
 323                    continue;
 324                }
 325
 0326                var containers = container.Split(',');
 0327                if (containers.Length < 2)
 328                {
 329                    continue;
 330                }
 331
 0332                var path = mediaSource.Path;
 0333                string? fileExtensionContainer = null;
 334
 0335                if (!string.IsNullOrEmpty(path))
 336                {
 0337                    path = Path.GetExtension(path);
 0338                    if (!string.IsNullOrEmpty(path))
 339                    {
 0340                        path = Path.GetExtension(path);
 0341                        if (!string.IsNullOrEmpty(path))
 342                        {
 0343                            path = path.TrimStart('.');
 344                        }
 345
 0346                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase)
 347                        {
 0348                            fileExtensionContainer = path;
 349                        }
 350                    }
 351                }
 352
 0353                mediaSource.Container = fileExtensionContainer ?? containers[0];
 354            }
 0355        }
 356
 357        /// <inheritdoc />
 358        /// TODO refactor this to use the new SetItemByNameInfo.
 359        /// Some callers already have the counts extracted so no reason to retrieve them again.
 360        public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user =
 361        {
 0362            var dto = GetBaseItemDtoInternal(item, options, user);
 363
 0364            if (options.ContainsField(ItemFields.ItemCounts)
 0365                && taggedItems is not null
 0366                && taggedItems.Count != 0)
 367            {
 0368                SetItemByNameInfo(item, dto, taggedItems);
 369            }
 370
 0371            return dto;
 372        }
 373
 374        private void SetItemByNameInfo(BaseItemDto dto, User? user)
 375        {
 6376            if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
 377            {
 6378                return;
 379            }
 380
 0381            var query = new InternalItemsQuery(user)
 0382            {
 0383                Recursive = true,
 0384                DtoOptions = new DtoOptions(false) { EnableImages = false },
 0385                IncludeItemTypes = relatedItemKinds
 0386            };
 387
 0388            switch (dto.Type)
 389            {
 390                case BaseItemKind.Genre:
 391                case BaseItemKind.MusicGenre:
 0392                    query.GenreIds = [dto.Id];
 0393                    break;
 394                case BaseItemKind.MusicArtist:
 0395                    query.ArtistIds = [dto.Id];
 0396                    break;
 397                case BaseItemKind.Person:
 0398                    query.PersonIds = [dto.Id];
 0399                    break;
 400                case BaseItemKind.Studio:
 0401                    query.StudioIds = [dto.Id];
 0402                    break;
 403                case BaseItemKind.Year
 0404                    when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
 0405                    query.Years = [year];
 0406                    break;
 407                default:
 0408                    return;
 409            }
 410
 0411            var counts = _libraryManager.GetItemCounts(query);
 412
 0413            dto.AlbumCount = counts.AlbumCount;
 0414            dto.ArtistCount = counts.ArtistCount;
 0415            dto.EpisodeCount = counts.EpisodeCount;
 0416            dto.MovieCount = counts.MovieCount;
 0417            dto.MusicVideoCount = counts.MusicVideoCount;
 0418            dto.ProgramCount = counts.ProgramCount;
 0419            dto.SeriesCount = counts.SeriesCount;
 0420            dto.SongCount = counts.SongCount;
 0421            dto.TrailerCount = counts.TrailerCount;
 0422            dto.ChildCount = counts.TotalItemCount();
 0423        }
 424
 425        private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
 426        {
 0427            if (item is MusicArtist)
 428            {
 0429                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0430                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0431                dto.SongCount = taggedItems.Count(i => i is Audio);
 432            }
 0433            else if (item is MusicGenre)
 434            {
 0435                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
 0436                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0437                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0438                dto.SongCount = taggedItems.Count(i => i is Audio);
 439            }
 440            else
 441            {
 442                // This populates them all and covers Genre, Person, Studio, Year
 443
 0444                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
 0445                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
 0446                dto.EpisodeCount = taggedItems.Count(i => i is Episode);
 0447                dto.MovieCount = taggedItems.Count(i => i is Movie);
 0448                dto.TrailerCount = taggedItems.Count(i => i is Trailer);
 0449                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
 0450                dto.SeriesCount = taggedItems.Count(i => i is Series);
 0451                dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram);
 0452                dto.SongCount = taggedItems.Count(i => i is Audio);
 453            }
 454
 0455            dto.ChildCount = taggedItems.Count;
 0456        }
 457
 458        /// <summary>
 459        /// Attaches the user specific info.
 460        /// </summary>
 461        private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
 462        {
 9463            if (item.IsFolder)
 464            {
 9465                var folder = (Folder)item;
 466
 9467                if (options.EnableUserData)
 468                {
 9469                    dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
 470                }
 471
 9472                if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
 473                {
 474                    // For these types we can try to optimize and assume these values will be equal
 9475                    if (item is MusicAlbum || item is Season || item is Playlist)
 476                    {
 0477                        dto.ChildCount = dto.RecursiveItemCount;
 0478                        var folderChildCount = folder.LinkedChildren.Length;
 479                        // The default is an empty array, so we can't reliably use the count when it's empty
 0480                        if (folderChildCount > 0)
 481                        {
 0482                            dto.ChildCount ??= folderChildCount;
 483                        }
 484                    }
 485
 9486                    if (options.ContainsField(ItemFields.ChildCount))
 487                    {
 6488                        dto.ChildCount ??= GetChildCount(folder, user);
 489                    }
 490                }
 491
 9492                if (options.ContainsField(ItemFields.CumulativeRunTimeTicks))
 493                {
 6494                    dto.CumulativeRunTimeTicks = item.RunTimeTicks;
 495                }
 496
 9497                if (options.ContainsField(ItemFields.DateLastMediaAdded))
 498                {
 6499                    dto.DateLastMediaAdded = folder.DateLastMediaAdded;
 500                }
 501            }
 502            else
 503            {
 0504                if (options.EnableUserData)
 505                {
 0506                    dto.UserData = _userDataRepository.GetUserDataDto(item, user);
 507                }
 508            }
 509
 9510            if (options.ContainsField(ItemFields.PlayAccess))
 511            {
 6512                dto.PlayAccess = item.GetPlayAccess(user);
 513            }
 9514        }
 515
 516        private static int GetChildCount(Folder folder, User user)
 517        {
 518            // Right now this is too slow to calculate for top level folders on a per-user basis
 519            // Just return something so that apps that are expecting a value won't think the folders are empty
 6520            if (folder is ICollectionFolder || folder is UserView)
 521            {
 0522                return Random.Shared.Next(1, 10);
 523            }
 524
 6525            return folder.GetChildCount(user);
 526        }
 527
 528        private static void SetBookProperties(BaseItemDto dto, Book item)
 529        {
 0530            dto.SeriesName = item.SeriesName;
 0531        }
 532
 533        private static void SetPhotoProperties(BaseItemDto dto, Photo item)
 534        {
 0535            dto.CameraMake = item.CameraMake;
 0536            dto.CameraModel = item.CameraModel;
 0537            dto.Software = item.Software;
 0538            dto.ExposureTime = item.ExposureTime;
 0539            dto.FocalLength = item.FocalLength;
 0540            dto.ImageOrientation = item.Orientation;
 0541            dto.Aperture = item.Aperture;
 0542            dto.ShutterSpeed = item.ShutterSpeed;
 543
 0544            dto.Latitude = item.Latitude;
 0545            dto.Longitude = item.Longitude;
 0546            dto.Altitude = item.Altitude;
 0547            dto.IsoSpeedRating = item.IsoSpeedRating;
 548
 0549            var album = item.AlbumEntity;
 550
 0551            if (album is not null)
 552            {
 0553                dto.Album = album.Name;
 0554                dto.AlbumId = album.Id;
 555            }
 0556        }
 557
 558        private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
 559        {
 0560            if (!string.IsNullOrEmpty(item.Album))
 561            {
 0562                var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
 0563                {
 0564                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
 0565                    Name = item.Album,
 0566                    Limit = 1
 0567                });
 568
 0569                if (parentAlbumIds.Count > 0)
 570                {
 0571                    dto.AlbumId = parentAlbumIds[0];
 572                }
 573            }
 574
 0575            dto.Album = item.Album;
 0576        }
 577
 578        private string[] GetImageTags(BaseItem item, List<ItemImageInfo> images)
 579        {
 9580            return images
 9581                .Select(p => GetImageCacheTag(item, p))
 9582                .Where(i => i is not null)
 9583                .ToArray()!; // null values got filtered out
 584        }
 585
 586        private string? GetImageCacheTag(BaseItem item, ItemImageInfo image)
 587        {
 588            try
 589            {
 0590                return _imageProcessor.GetImageCacheTag(item, image);
 591            }
 0592            catch (Exception ex)
 593            {
 0594                _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
 0595                return null;
 596            }
 0597        }
 598
 599        /// <summary>
 600        /// Attaches People DTO's to a DTOBaseItem.
 601        /// </summary>
 602        /// <param name="dto">The dto.</param>
 603        /// <param name="item">The item.</param>
 604        /// <param name="user">The requesting user.</param>
 605        private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
 606        {
 607            // Ordering by person type to ensure actors and artists are at the front.
 608            // This is taking advantage of the fact that they both begin with A
 609            // This should be improved in the future
 6610            var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
 6611                .ThenBy(i =>
 6612                {
 6613                    if (i.IsType(PersonKind.Actor))
 6614                    {
 6615                        return 0;
 6616                    }
 6617
 6618                    if (i.IsType(PersonKind.GuestStar))
 6619                    {
 6620                        return 1;
 6621                    }
 6622
 6623                    if (i.IsType(PersonKind.Director))
 6624                    {
 6625                        return 2;
 6626                    }
 6627
 6628                    if (i.IsType(PersonKind.Writer))
 6629                    {
 6630                        return 3;
 6631                    }
 6632
 6633                    if (i.IsType(PersonKind.Producer))
 6634                    {
 6635                        return 4;
 6636                    }
 6637
 6638                    if (i.IsType(PersonKind.Composer))
 6639                    {
 6640                        return 4;
 6641                    }
 6642
 6643                    return 10;
 6644                })
 6645                .ToList();
 646
 6647            var list = new List<BaseItemPerson>();
 648
 6649            Dictionary<string, Person> dictionary = people.Select(p => p.Name)
 6650                .Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
 6651                {
 6652                    try
 6653                    {
 6654                        return _libraryManager.GetPerson(c);
 6655                    }
 6656                    catch (Exception ex)
 6657                    {
 6658                        _logger.LogError(ex, "Error getting person {Name}", c);
 6659                        return null;
 6660                    }
 6661                }).Where(i => i is not null)
 6662                .Where(i => user is null || i!.IsVisible(user))
 6663                .DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase)
 6664                .ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out
 665
 12666            for (var i = 0; i < people.Count; i++)
 667            {
 0668                var person = people[i];
 669
 0670                var baseItemPerson = new BaseItemPerson
 0671                {
 0672                    Name = person.Name,
 0673                    Role = person.Role,
 0674                    Type = person.Type
 0675                };
 676
 0677                if (dictionary.TryGetValue(person.Name, out Person? entity))
 678                {
 0679                    baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
 0680                    baseItemPerson.Id = entity.Id;
 0681                    if (dto.ImageBlurHashes is not null)
 682                    {
 683                        // Only add BlurHash for the person's image.
 0684                        baseItemPerson.ImageBlurHashes = [];
 0685                        foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
 686                        {
 0687                            if (blurHash is not null)
 688                            {
 0689                                baseItemPerson.ImageBlurHashes[imageType] = [];
 0690                                foreach (var (imageId, blurHashValue) in blurHash)
 691                                {
 0692                                    if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalI
 693                                    {
 0694                                        baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
 695                                    }
 696                                }
 697                            }
 698                        }
 699                    }
 700
 0701                    list.Add(baseItemPerson);
 702                }
 703            }
 704
 6705            dto.People = list.ToArray();
 6706        }
 707
 708        /// <summary>
 709        /// Attaches the studios.
 710        /// </summary>
 711        /// <param name="dto">The dto.</param>
 712        /// <param name="item">The item.</param>
 713        private void AttachStudios(BaseItemDto dto, BaseItem item)
 714        {
 6715            dto.Studios = item.Studios
 6716                .Where(i => !string.IsNullOrEmpty(i))
 6717                .Select(i => new NameGuidPair
 6718                {
 6719                    Name = i,
 6720                    Id = _libraryManager.GetStudioId(i)
 6721                })
 6722                .ToArray();
 6723        }
 724
 725        private void AttachGenreItems(BaseItemDto dto, BaseItem item)
 726        {
 6727            dto.GenreItems = item.Genres
 6728                .Where(i => !string.IsNullOrEmpty(i))
 6729                .Select(i => new NameGuidPair
 6730                {
 6731                    Name = i,
 6732                    Id = GetGenreId(i, item)
 6733                })
 6734                .ToArray();
 6735        }
 736
 737        private Guid GetGenreId(string name, BaseItem owner)
 738        {
 0739            if (owner is IHasMusicGenres)
 740            {
 0741                return _libraryManager.GetMusicGenreId(name);
 742            }
 743
 0744            return _libraryManager.GetGenreId(name);
 745        }
 746
 747        private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
 748        {
 0749            var image = item.GetImageInfo(imageType, imageIndex);
 0750            if (image is not null)
 751            {
 0752                return GetTagAndFillBlurhash(dto, item, image);
 753            }
 754
 0755            return null;
 756        }
 757
 758        private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
 759        {
 0760            var tag = GetImageCacheTag(item, image);
 0761            if (tag is null)
 762            {
 0763                return null;
 764            }
 765
 0766            if (!string.IsNullOrEmpty(image.BlurHash))
 767            {
 0768                dto.ImageBlurHashes ??= [];
 769
 0770                if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
 771                {
 0772                    value = [];
 0773                    dto.ImageBlurHashes[image.Type] = value;
 774                }
 775
 0776                value[tag] = image.BlurHash;
 777            }
 778
 0779            return tag;
 780        }
 781
 782        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit)
 783        {
 9784            return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList());
 785        }
 786
 787        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInf
 788        {
 9789            var tags = GetImageTags(item, images);
 9790            var hashes = new Dictionary<string, string>();
 18791            for (int i = 0; i < images.Count; i++)
 792            {
 0793                var img = images[i];
 0794                if (!string.IsNullOrEmpty(img.BlurHash))
 795                {
 0796                    var tag = tags[i];
 0797                    hashes[tag] = img.BlurHash;
 798                }
 799            }
 800
 9801            if (hashes.Count > 0)
 802            {
 0803                dto.ImageBlurHashes ??= [];
 804
 0805                dto.ImageBlurHashes[imageType] = hashes;
 806            }
 807
 9808            return tags;
 809        }
 810
 811        /// <summary>
 812        /// Sets simple property values on a DTOBaseItem.
 813        /// </summary>
 814        /// <param name="dto">The dto.</param>
 815        /// <param name="item">The item.</param>
 816        /// <param name="owner">The owner.</param>
 817        /// <param name="options">The options.</param>
 818        private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
 819        {
 9820            if (options.ContainsField(ItemFields.DateCreated))
 821            {
 6822                dto.DateCreated = item.DateCreated;
 823            }
 824
 9825            if (options.ContainsField(ItemFields.Settings))
 826            {
 6827                dto.LockedFields = item.LockedFields;
 6828                dto.LockData = item.IsLocked;
 6829                dto.ForcedSortName = item.ForcedSortName;
 830            }
 831
 9832            dto.Container = item.Container;
 9833            dto.EndDate = item.EndDate;
 834
 9835            if (options.ContainsField(ItemFields.ExternalUrls))
 836            {
 6837                dto.ExternalUrls = _providerManager.GetExternalUrls(item).ToArray();
 838            }
 839
 9840            if (options.ContainsField(ItemFields.Tags))
 841            {
 6842                dto.Tags = item.Tags;
 843            }
 844
 9845            if (item is IHasAspectRatio hasAspectRatio)
 846            {
 0847                dto.AspectRatio = hasAspectRatio.AspectRatio;
 848            }
 849
 9850            dto.ImageBlurHashes = [];
 851
 9852            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
 9853            if (backdropLimit > 0)
 854            {
 9855                dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
 856            }
 857
 9858            if (options.ContainsField(ItemFields.Genres))
 859            {
 6860                dto.Genres = item.Genres;
 6861                AttachGenreItems(dto, item);
 862            }
 863
 9864            if (options.EnableImages)
 865            {
 9866                dto.ImageTags = [];
 867
 868                // Prevent implicitly captured closure
 9869                var currentItem = item;
 18870                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)))
 871                {
 0872                    if (options.GetImageLimit(image.Type) > 0)
 873                    {
 0874                        var tag = GetTagAndFillBlurhash(dto, item, image);
 875
 0876                        if (tag is not null)
 877                        {
 0878                            dto.ImageTags[image.Type] = tag;
 879                        }
 880                    }
 881                }
 882            }
 883
 9884            dto.Id = item.Id;
 9885            dto.IndexNumber = item.IndexNumber;
 9886            dto.ParentIndexNumber = item.ParentIndexNumber;
 887
 9888            if (item.IsFolder)
 889            {
 9890                dto.IsFolder = true;
 891            }
 0892            else if (item is IHasMediaSources)
 893            {
 0894                dto.IsFolder = false;
 895            }
 896
 9897            dto.MediaType = item.MediaType;
 898
 9899            if (item is not LiveTvProgram)
 900            {
 9901                dto.LocationType = item.LocationType;
 902            }
 903
 9904            dto.Audio = item.Audio;
 905
 9906            if (options.ContainsField(ItemFields.Settings))
 907            {
 6908                dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 6909                dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 910            }
 911
 9912            dto.CriticRating = item.CriticRating;
 913
 9914            if (item is IHasDisplayOrder hasDisplayOrder)
 915            {
 0916                dto.DisplayOrder = hasDisplayOrder.DisplayOrder;
 917            }
 918
 9919            if (item is IHasCollectionType hasCollectionType)
 920            {
 3921                dto.CollectionType = hasCollectionType.CollectionType;
 922            }
 923
 9924            if (options.ContainsField(ItemFields.RemoteTrailers))
 925            {
 6926                dto.RemoteTrailers = item.RemoteTrailers;
 927            }
 928
 9929            dto.Name = item.Name;
 9930            dto.OfficialRating = item.OfficialRating;
 931
 9932            if (options.ContainsField(ItemFields.Overview))
 933            {
 6934                dto.Overview = item.Overview;
 935            }
 936
 9937            if (options.ContainsField(ItemFields.OriginalTitle))
 938            {
 6939                dto.OriginalTitle = item.OriginalTitle;
 940            }
 941
 9942            if (options.ContainsField(ItemFields.ParentId))
 943            {
 6944                dto.ParentId = item.DisplayParentId;
 945            }
 946
 9947            AddInheritedImages(dto, item, options, owner);
 948
 9949            if (options.ContainsField(ItemFields.Path))
 950            {
 6951                dto.Path = GetMappedPath(item, owner);
 952            }
 953
 9954            if (options.ContainsField(ItemFields.EnableMediaSourceDisplay))
 955            {
 6956                dto.EnableMediaSourceDisplay = item.EnableMediaSourceDisplay;
 957            }
 958
 9959            dto.PremiereDate = item.PremiereDate;
 9960            dto.ProductionYear = item.ProductionYear;
 961
 9962            if (options.ContainsField(ItemFields.ProviderIds))
 963            {
 6964                dto.ProviderIds = item.ProviderIds;
 965            }
 966
 9967            dto.RunTimeTicks = item.RunTimeTicks;
 968
 9969            if (options.ContainsField(ItemFields.SortName))
 970            {
 6971                dto.SortName = item.SortName;
 972            }
 973
 9974            if (options.ContainsField(ItemFields.CustomRating))
 975            {
 6976                dto.CustomRating = item.CustomRating;
 977            }
 978
 9979            if (options.ContainsField(ItemFields.Taglines))
 980            {
 6981                if (!string.IsNullOrEmpty(item.Tagline))
 982                {
 0983                    dto.Taglines = new string[] { item.Tagline };
 984                }
 985
 6986                dto.Taglines ??= Array.Empty<string>();
 987            }
 988
 9989            dto.Type = item.GetBaseItemKind();
 9990            if ((item.CommunityRating ?? 0) > 0)
 991            {
 0992                dto.CommunityRating = item.CommunityRating;
 993            }
 994
 9995            if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder)
 996            {
 0997                dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
 998            }
 999
 91000            if (item.LUFS.HasValue)
 1001            {
 1002                // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
 01003                dto.NormalizationGain = -18f - item.LUFS;
 1004            }
 91005            else if (item.NormalizationGain.HasValue)
 1006            {
 01007                dto.NormalizationGain = item.NormalizationGain;
 1008            }
 1009
 1010            // Add audio info
 91011            if (item is Audio audio)
 1012            {
 01013                dto.Album = audio.Album;
 01014                dto.ExtraType = audio.ExtraType;
 1015
 01016                var albumParent = audio.AlbumEntity;
 1017
 01018                if (albumParent is not null)
 1019                {
 01020                    dto.AlbumId = albumParent.Id;
 01021                    dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
 01022                    if (albumParent.LUFS.HasValue)
 1023                    {
 1024                        // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
 01025                        dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
 1026                    }
 01027                    else if (albumParent.NormalizationGain.HasValue)
 1028                    {
 01029                        dto.AlbumNormalizationGain = albumParent.NormalizationGain;
 1030                    }
 1031                }
 1032
 1033                // if (options.ContainsField(ItemFields.MediaSourceCount))
 1034                // {
 1035                // Songs always have one
 1036                // }
 1037            }
 1038
 91039            if (item is IHasArtist hasArtist)
 1040            {
 01041                dto.Artists = hasArtist.Artists;
 1042
 1043                // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
 1044                // {
 1045                //    EnableTotalRecordCount = false,
 1046                //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
 1047                // });
 1048
 1049                // dto.ArtistItems = artistItems.Items
 1050                //    .Select(i =>
 1051                //    {
 1052                //        var artist = i.Item1;
 1053                //        return new NameIdPair
 1054                //        {
 1055                //            Name = artist.Name,
 1056                //            Id = artist.Id.ToString("N", CultureInfo.InvariantCulture)
 1057                //        };
 1058                //    })
 1059                //    .ToList();
 1060
 1061                // Include artists that are not in the database yet, e.g., just added via metadata editor
 1062                // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
 01063                var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpa
 1064
 01065                dto.ArtistItems = hasArtist.Artists
 01066                    .Where(name => !string.IsNullOrWhiteSpace(name))
 01067                    .Distinct()
 01068                    .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
 01069                        ? new NameGuidPair { Name = name, Id = artists[0].Id }
 01070                        : null)
 01071                    .Where(item => item is not null)
 01072                    .ToArray();
 1073            }
 1074
 91075            if (item is IHasAlbumArtist hasAlbumArtist)
 1076            {
 01077                dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
 1078
 1079                // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
 1080                // {
 1081                //    EnableTotalRecordCount = false,
 1082                //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
 1083                // });
 1084
 1085                // dto.AlbumArtists = artistItems.Items
 1086                //    .Select(i =>
 1087                //    {
 1088                //        var artist = i.Item1;
 1089                //        return new NameIdPair
 1090                //        {
 1091                //            Name = artist.Name,
 1092                //            Id = artist.Id.ToString("N", CultureInfo.InvariantCulture)
 1093                //        };
 1094                //    })
 1095                //    .ToList();
 1096
 01097                var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.I
 1098
 01099                dto.AlbumArtists = hasAlbumArtist.AlbumArtists
 01100                    .Where(name => !string.IsNullOrWhiteSpace(name))
 01101                    .Distinct()
 01102                    .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 
 01103                        ? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
 01104                        : null)
 01105                    .Where(item => item is not null)
 01106                    .ToArray();
 1107            }
 1108
 1109            // Add video info
 91110            if (item is Video video)
 1111            {
 01112                dto.VideoType = video.VideoType;
 01113                dto.Video3DFormat = video.Video3DFormat;
 01114                dto.IsoType = video.IsoType;
 1115
 01116                if (video.HasSubtitles)
 1117                {
 01118                    dto.HasSubtitles = video.HasSubtitles;
 1119                }
 1120
 01121                if (video.AdditionalParts.Length != 0)
 1122                {
 01123                    dto.PartCount = video.AdditionalParts.Length + 1;
 1124                }
 1125
 01126                if (options.ContainsField(ItemFields.MediaSourceCount))
 1127                {
 01128                    var mediaSourceCount = video.MediaSourceCount;
 01129                    if (mediaSourceCount != 1)
 1130                    {
 01131                        dto.MediaSourceCount = mediaSourceCount;
 1132                    }
 1133                }
 1134
 01135                if (options.ContainsField(ItemFields.Chapters))
 1136                {
 01137                    dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
 1138                }
 1139
 01140                if (options.ContainsField(ItemFields.Trickplay))
 1141                {
 01142                    var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
 01143                    dto.Trickplay = trickplay.ToDictionary(
 01144                        mediaStream => mediaStream.Key,
 01145                        mediaStream => mediaStream.Value.ToDictionary(
 01146                            width => width.Key,
 01147                            width => new TrickplayInfoDto(width.Value)));
 1148                }
 1149
 01150                dto.ExtraType = video.ExtraType;
 1151            }
 1152
 91153            if (options.ContainsField(ItemFields.MediaStreams))
 1154            {
 1155                // Add VideoInfo
 61156                if (item is IHasMediaSources)
 1157                {
 1158                    MediaStream[] mediaStreams;
 1159
 01160                    if (dto.MediaSources is not null && dto.MediaSources.Length > 0)
 1161                    {
 01162                        if (item.SourceType == SourceType.Channel)
 1163                        {
 01164                            mediaStreams = dto.MediaSources[0].MediaStreams.ToArray();
 1165                        }
 1166                        else
 1167                        {
 01168                            string id = item.Id.ToString("N", CultureInfo.InvariantCulture);
 01169                            mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalI
 01170                                .SelectMany(i => i.MediaStreams)
 01171                                .ToArray();
 1172                        }
 1173                    }
 1174                    else
 1175                    {
 01176                        mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
 1177                    }
 1178
 01179                    dto.MediaStreams = mediaStreams;
 1180                }
 1181            }
 1182
 91183            BaseItem[]? allExtras = null;
 1184
 91185            if (options.ContainsField(ItemFields.SpecialFeatureCount))
 1186            {
 61187                allExtras = item.GetExtras().ToArray();
 61188                dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contai
 1189            }
 1190
 91191            if (options.ContainsField(ItemFields.LocalTrailerCount))
 1192            {
 61193                if (item is IHasTrailers hasTrailers)
 1194                {
 01195                    dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
 1196                }
 1197                else
 1198                {
 61199                    dto.LocalTrailerCount = (allExtras ?? item.GetExtras()).Count(i => i.ExtraType == ExtraType.Trailer)
 1200                }
 1201            }
 1202
 1203            // Add EpisodeInfo
 91204            if (item is Episode episode)
 1205            {
 01206                dto.IndexNumberEnd = episode.IndexNumberEnd;
 01207                dto.SeriesName = episode.SeriesName;
 1208
 01209                if (options.ContainsField(ItemFields.SpecialEpisodeNumbers))
 1210                {
 01211                    dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber;
 01212                    dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber;
 01213                    dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber;
 1214                }
 1215
 01216                dto.SeasonName = episode.SeasonName;
 01217                dto.SeasonId = episode.SeasonId;
 01218                dto.SeriesId = episode.SeriesId;
 1219
 01220                Series? episodeSeries = null;
 1221
 1222                // this block will add the series poster for episodes without a poster
 1223                // TODO maybe remove the if statement entirely
 1224                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
 1225                {
 01226                    episodeSeries ??= episode.Series;
 01227                    if (episodeSeries is not null)
 1228                    {
 01229                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
 01230                        if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
 1231                        {
 01232                            AttachPrimaryImageAspectRatio(dto, episodeSeries);
 1233                        }
 1234                    }
 1235                }
 1236
 01237                if (options.ContainsField(ItemFields.SeriesStudio))
 1238                {
 01239                    episodeSeries ??= episode.Series;
 01240                    if (episodeSeries is not null)
 1241                    {
 01242                        dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
 1243                    }
 1244                }
 1245            }
 1246
 1247            // Add SeriesInfo
 1248            Series? series;
 91249            if (item is Series tmp)
 1250            {
 01251                series = tmp;
 01252                dto.AirDays = series.AirDays;
 01253                dto.AirTime = series.AirTime;
 01254                dto.Status = series.Status?.ToString();
 1255            }
 1256
 1257            // Add SeasonInfo
 91258            if (item is Season season)
 1259            {
 01260                dto.SeriesName = season.SeriesName;
 01261                dto.SeriesId = season.SeriesId;
 1262
 01263                series = null;
 1264
 01265                if (options.ContainsField(ItemFields.SeriesStudio))
 1266                {
 01267                    series ??= season.Series;
 01268                    if (series is not null)
 1269                    {
 01270                        dto.SeriesStudio = series.Studios.FirstOrDefault();
 1271                    }
 1272                }
 1273
 1274                // this block will add the series poster for seasons without a poster
 1275                // TODO maybe remove the if statement entirely
 1276                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
 1277                {
 01278                    series ??= season.Series;
 01279                    if (series is not null)
 1280                    {
 01281                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
 01282                        if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
 1283                        {
 01284                            AttachPrimaryImageAspectRatio(dto, series);
 1285                        }
 1286                    }
 1287                }
 1288            }
 1289
 91290            if (item is MusicVideo musicVideo)
 1291            {
 01292                SetMusicVideoProperties(dto, musicVideo);
 1293            }
 1294
 91295            if (item is Book book)
 1296            {
 01297                SetBookProperties(dto, book);
 1298            }
 1299
 91300            if (options.ContainsField(ItemFields.ProductionLocations))
 1301            {
 61302                if (item.ProductionLocations.Length > 0 || item is Movie)
 1303                {
 01304                    dto.ProductionLocations = item.ProductionLocations;
 1305                }
 1306            }
 1307
 91308            if (options.ContainsField(ItemFields.Width))
 1309            {
 61310                var width = item.Width;
 61311                if (width > 0)
 1312                {
 01313                    dto.Width = width;
 1314                }
 1315            }
 1316
 91317            if (options.ContainsField(ItemFields.Height))
 1318            {
 61319                var height = item.Height;
 61320                if (height > 0)
 1321                {
 01322                    dto.Height = height;
 1323                }
 1324            }
 1325
 91326            if (options.ContainsField(ItemFields.IsHD))
 1327            {
 1328                // Compatibility
 61329                if (item.IsHD)
 1330                {
 01331                    dto.IsHD = true;
 1332                }
 1333            }
 1334
 91335            if (item is Photo photo)
 1336            {
 01337                SetPhotoProperties(dto, photo);
 1338            }
 1339
 91340            dto.ChannelId = item.ChannelId;
 1341
 91342            if (item.SourceType == SourceType.Channel)
 1343            {
 01344                var channel = _libraryManager.GetItemById(item.ChannelId);
 01345                if (channel is not null)
 1346                {
 01347                    dto.ChannelName = channel.Name;
 1348                }
 1349            }
 91350        }
 1351
 1352        private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
 1353        {
 01354            if (currentItem is MusicAlbum musicAlbum)
 1355            {
 01356                var artist = musicAlbum.GetMusicArtist(new DtoOptions(false));
 01357                if (artist is not null)
 1358                {
 01359                    return artist;
 1360                }
 1361            }
 1362
 01363            var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
 1364
 01365            if (parent is null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is 
 1366            {
 01367                parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
 1368            }
 1369
 01370            return parent;
 1371        }
 1372
 1373        private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
 1374        {
 91375            if (!item.SupportsInheritedParentImages)
 1376            {
 91377                return;
 1378            }
 1379
 01380            var logoLimit = options.GetImageLimit(ImageType.Logo);
 01381            var artLimit = options.GetImageLimit(ImageType.Art);
 01382            var thumbLimit = options.GetImageLimit(ImageType.Thumb);
 01383            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
 1384
 1385            // For now. Emby apps are not using this
 01386            artLimit = 0;
 1387
 01388            if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0)
 1389            {
 01390                return;
 1391            }
 1392
 01393            BaseItem? parent = null;
 01394            var isFirst = true;
 1395
 01396            var imageTags = dto.ImageTags;
 1397
 01398            while ((!(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
 01399                || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
 01400                || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
 01401                || parent is Series)
 1402            {
 01403                parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
 01404                if (parent is null)
 1405                {
 1406                    break;
 1407                }
 1408
 01409                var allImages = parent.ImageInfos;
 1410
 01411                if (logoLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogo
 1412                {
 01413                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
 1414
 01415                    if (image is not null)
 1416                    {
 01417                        dto.ParentLogoItemId = parent.Id;
 01418                        dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1419                    }
 1420                }
 1421
 01422                if (artLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtIte
 1423                {
 01424                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
 1425
 01426                    if (image is not null)
 1427                    {
 01428                        dto.ParentArtItemId = parent.Id;
 01429                        dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1430                    }
 1431                }
 1432
 01433                if (thumbLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentT
 1434                {
 01435                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
 1436
 01437                    if (image is not null)
 1438                    {
 01439                        dto.ParentThumbItemId = parent.Id;
 01440                        dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
 1441                    }
 1442                }
 1443
 01444                if (backdropLimit > 0 && !((dto.BackdropImageTags is not null && dto.BackdropImageTags.Length > 0) || (d
 1445                {
 01446                    var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
 1447
 01448                    if (images.Count > 0)
 1449                    {
 01450                        dto.ParentBackdropItemId = parent.Id;
 01451                        dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
 1452                    }
 1453                }
 1454
 01455                isFirst = false;
 1456
 01457                if (!parent.SupportsInheritedParentImages)
 1458                {
 1459                    break;
 1460                }
 1461
 01462                parent = GetImageDisplayParent(parent, item);
 1463            }
 01464        }
 1465
 1466        private string GetMappedPath(BaseItem item, BaseItem? ownerItem)
 1467        {
 61468            var path = item.Path;
 1469
 61470            if (item.IsFileProtocol)
 1471            {
 61472                path = _libraryManager.GetPathAfterNetworkSubstitution(path, ownerItem ?? item);
 1473            }
 1474
 61475            return path;
 1476        }
 1477
 1478        /// <summary>
 1479        /// Attaches the primary image aspect ratio.
 1480        /// </summary>
 1481        /// <param name="dto">The dto.</param>
 1482        /// <param name="item">The item.</param>
 1483        public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
 1484        {
 61485            dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
 61486        }
 1487
 1488        public double? GetPrimaryImageAspectRatio(BaseItem item)
 1489        {
 61490            var imageInfo = item.GetImageInfo(ImageType.Primary, 0);
 1491
 61492            if (imageInfo is null)
 1493            {
 61494                return null;
 1495            }
 1496
 01497            if (!imageInfo.IsLocalFile)
 1498            {
 01499                return item.GetDefaultPrimaryImageAspectRatio();
 1500            }
 1501
 1502            try
 1503            {
 01504                var size = _imageProcessor.GetImageDimensions(item, imageInfo);
 01505                var width = size.Width;
 01506                var height = size.Height;
 01507                if (width > 0 && height > 0)
 1508                {
 01509                    return (double)width / height;
 1510                }
 01511            }
 01512            catch (Exception ex)
 1513            {
 01514                _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
 01515            }
 1516
 01517            return item.GetDefaultPrimaryImageAspectRatio();
 01518        }
 1519    }
 1520}

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)
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)
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)
GetChildCount(MediaBrowser.Controller.Entities.Folder,Jellyfin.Database.Implementations.Entities.User)
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)
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)