#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Movie = MediaBrowser.Controller.Entities.Movies.Movie; using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; using Person = MediaBrowser.Controller.Entities.Person; using Photo = MediaBrowser.Controller.Entities.Photo; using Season = MediaBrowser.Controller.Entities.TV.Season; using Series = MediaBrowser.Controller.Entities.TV.Series; namespace Emby.Server.Implementations.Dto { public class DtoService : IDtoService { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; private readonly IItemRepository _itemRepo; private readonly IImageProcessor _imageProcessor; private readonly IProviderManager _providerManager; private readonly IApplicationHost _appHost; private readonly IMediaSourceManager _mediaSourceManager; private readonly Lazy _livetvManagerFactory; private readonly ILyricManager _lyricManager; public DtoService( ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IProviderManager providerManager, IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy livetvManagerFactory, ILyricManager lyricManager) { _logger = logger; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _itemRepo = itemRepo; _imageProcessor = imageProcessor; _providerManager = providerManager; _appHost = appHost; _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _lyricManager = lyricManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; /// public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null) { var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList(); var returnItems = new BaseItemDto[accessibleItems.Count]; List<(BaseItem, BaseItemDto)> programTuples = null; List<(BaseItemDto, LiveTvChannel)> channelTuples = null; for (int index = 0; index < accessibleItems.Count; index++) { var item = accessibleItems[index]; var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) { (channelTuples ??= new()).Add((dto, tvChannel)); } else if (item is LiveTvProgram) { (programTuples ??= new()).Add((item, dto)); } if (item is IItemByName byName) { if (options.ContainsField(ItemFields.ItemCounts)) { var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user) { Recursive = true, DtoOptions = new DtoOptions(false) { EnableImages = false } }); SetItemByNameInfo(item, dto, libraryItems); } } returnItems[index] = dto; } if (programTuples is not null) { LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); } if (channelTuples is not null) { LivetvManager.AddChannelInfo(channelTuples, options, user); } return returnItems; } public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) { var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) { LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user); } else if (item is LiveTvProgram) { LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); } else if (item is Audio) { dto.HasLyrics = _lyricManager.HasLyricFile(item); } if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts)) { SetItemByNameInfo( item, dto, GetTaggedItems( itemByName, user, new DtoOptions(false) { EnableImages = false })); } return dto; } private static IList GetTaggedItems(IItemByName byName, User user, DtoOptions options) { return byName.GetTaggedItems( new InternalItemsQuery(user) { Recursive = true, DtoOptions = options }); } private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) { var dto = new BaseItemDto { ServerId = _appHost.SystemId }; if (item.SourceType == SourceType.Channel) { dto.SourceType = item.SourceType.ToString(); } if (options.ContainsField(ItemFields.People)) { AttachPeople(dto, item, user); } if (options.ContainsField(ItemFields.PrimaryImageAspectRatio)) { try { AttachPrimaryImageAspectRatio(dto, item); } catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name); } } if (options.ContainsField(ItemFields.DisplayPreferencesId)) { dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture); } if (user is not null) { AttachUserSpecificInfo(dto, item, user, options); } if (item is IHasMediaSources && options.ContainsField(ItemFields.MediaSources)) { dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray(); NormalizeMediaSourceContainers(dto); } if (options.ContainsField(ItemFields.Studios)) { AttachStudios(dto, item); } AttachBasicFields(dto, item, owner, options); if (options.ContainsField(ItemFields.CanDelete)) { dto.CanDelete = user is null ? item.CanDelete() : item.CanDelete(user); } if (options.ContainsField(ItemFields.CanDownload)) { dto.CanDownload = user is null ? item.CanDownload() : item.CanDownload(user); } if (options.ContainsField(ItemFields.Etag)) { dto.Etag = item.GetEtag(user); } var liveTvManager = LivetvManager; var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); if (activeRecording is not null) { dto.Type = BaseItemKind.Recording; dto.CanDownload = false; dto.RunTimeTicks = null; if (!string.IsNullOrEmpty(dto.SeriesName)) { dto.EpisodeTitle = dto.Name; dto.Name = dto.SeriesName; } liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user); } return dto; } private static void NormalizeMediaSourceContainers(BaseItemDto dto) { foreach (var mediaSource in dto.MediaSources) { var container = mediaSource.Container; if (string.IsNullOrEmpty(container)) { continue; } var containers = container.Split(','); if (containers.Length < 2) { continue; } var path = mediaSource.Path; string fileExtensionContainer = null; if (!string.IsNullOrEmpty(path)) { path = Path.GetExtension(path); if (!string.IsNullOrEmpty(path)) { path = Path.GetExtension(path); if (!string.IsNullOrEmpty(path)) { path = path.TrimStart('.'); } if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase)) { fileExtensionContainer = path; } } } mediaSource.Container = fileExtensionContainer ?? containers[0]; } } public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null) { var dto = GetBaseItemDtoInternal(item, options, user); if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts)) { SetItemByNameInfo(item, dto, taggedItems); } return dto; } private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList taggedItems) { if (item is MusicArtist) { dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum); dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo); dto.SongCount = taggedItems.Count(i => i is Audio); } else if (item is MusicGenre) { dto.ArtistCount = taggedItems.Count(i => i is MusicArtist); dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum); dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo); dto.SongCount = taggedItems.Count(i => i is Audio); } else { // This populates them all and covers Genre, Person, Studio, Year dto.ArtistCount = taggedItems.Count(i => i is MusicArtist); dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum); dto.EpisodeCount = taggedItems.Count(i => i is Episode); dto.MovieCount = taggedItems.Count(i => i is Movie); dto.TrailerCount = taggedItems.Count(i => i is Trailer); dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo); dto.SeriesCount = taggedItems.Count(i => i is Series); dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram); dto.SongCount = taggedItems.Count(i => i is Audio); } dto.ChildCount = taggedItems.Count; } /// /// Attaches the user specific info. /// private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options) { if (item.IsFolder) { var folder = (Folder)item; if (options.EnableUserData) { dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options); } if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library) { // For these types we can try to optimize and assume these values will be equal if (item is MusicAlbum || item is Season || item is Playlist) { dto.ChildCount = dto.RecursiveItemCount; var folderChildCount = folder.LinkedChildren.Length; // The default is an empty array, so we can't reliably use the count when it's empty if (folderChildCount > 0) { dto.ChildCount ??= folderChildCount; } } if (options.ContainsField(ItemFields.ChildCount)) { dto.ChildCount ??= GetChildCount(folder, user); } } if (options.ContainsField(ItemFields.CumulativeRunTimeTicks)) { dto.CumulativeRunTimeTicks = item.RunTimeTicks; } if (options.ContainsField(ItemFields.DateLastMediaAdded)) { dto.DateLastMediaAdded = folder.DateLastMediaAdded; } } else { if (options.EnableUserData) { dto.UserData = _userDataRepository.GetUserDataDto(item, user); } } if (options.ContainsField(ItemFields.PlayAccess)) { dto.PlayAccess = item.GetPlayAccess(user); } if (options.ContainsField(ItemFields.BasicSyncInfo)) { var userCanSync = user is not null && user.HasPermission(PermissionKind.EnableContentDownloading); if (userCanSync && item.SupportsExternalTransfer) { dto.SupportsSync = true; } } } private static int GetChildCount(Folder folder, User user) { // Right now this is too slow to calculate for top level folders on a per-user basis // Just return something so that apps that are expecting a value won't think the folders are empty if (folder is ICollectionFolder || folder is UserView) { return Random.Shared.Next(1, 10); } return folder.GetChildCount(user); } private static void SetBookProperties(BaseItemDto dto, Book item) { dto.SeriesName = item.SeriesName; } private static void SetPhotoProperties(BaseItemDto dto, Photo item) { dto.CameraMake = item.CameraMake; dto.CameraModel = item.CameraModel; dto.Software = item.Software; dto.ExposureTime = item.ExposureTime; dto.FocalLength = item.FocalLength; dto.ImageOrientation = item.Orientation; dto.Aperture = item.Aperture; dto.ShutterSpeed = item.ShutterSpeed; dto.Latitude = item.Latitude; dto.Longitude = item.Longitude; dto.Altitude = item.Altitude; dto.IsoSpeedRating = item.IsoSpeedRating; var album = item.AlbumEntity; if (album is not null) { dto.Album = album.Name; dto.AlbumId = album.Id; } } private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item) { if (!string.IsNullOrEmpty(item.Album)) { var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = item.Album, Limit = 1 }); if (parentAlbumIds.Count > 0) { dto.AlbumId = parentAlbumIds[0]; } } dto.Album = item.Album; } private string[] GetImageTags(BaseItem item, List images) { return images .Select(p => GetImageCacheTag(item, p)) .Where(i => i is not null) .ToArray(); } private string GetImageCacheTag(BaseItem item, ItemImageInfo image) { try { return _imageProcessor.GetImageCacheTag(item, image); } catch (Exception ex) { _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path); return null; } } /// /// Attaches People DTO's to a DTOBaseItem. /// /// The dto. /// The item. /// The requesting user. private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null) { // Ordering by person type to ensure actors and artists are at the front. // This is taking advantage of the fact that they both begin with A // This should be improved in the future var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue) .ThenBy(i => { if (i.IsType(PersonKind.Actor)) { return 0; } if (i.IsType(PersonKind.GuestStar)) { return 1; } if (i.IsType(PersonKind.Director)) { return 2; } if (i.IsType(PersonKind.Writer)) { return 3; } if (i.IsType(PersonKind.Producer)) { return 4; } if (i.IsType(PersonKind.Composer)) { return 4; } return 10; }) .ToList(); var list = new List(); var dictionary = people.Select(p => p.Name) .Distinct(StringComparer.OrdinalIgnoreCase).Select(c => { try { return _libraryManager.GetPerson(c); } catch (Exception ex) { _logger.LogError(ex, "Error getting person {Name}", c); return null; } }).Where(i => i is not null) .Where(i => user is null ? true : i.IsVisible(user)) .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); for (var i = 0; i < people.Count; i++) { var person = people[i]; var baseItemPerson = new BaseItemPerson { Name = person.Name, Role = person.Role, Type = person.Type }; if (dictionary.TryGetValue(person.Name, out Person entity)) { baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); baseItemPerson.Id = entity.Id; if (dto.ImageBlurHashes is not null) { // Only add BlurHash for the person's image. baseItemPerson.ImageBlurHashes = new Dictionary>(); foreach (var (imageType, blurHash) in dto.ImageBlurHashes) { if (blurHash is not null) { baseItemPerson.ImageBlurHashes[imageType] = new Dictionary(); foreach (var (imageId, blurHashValue) in blurHash) { if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase)) { baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue; } } } } } list.Add(baseItemPerson); } } dto.People = list.ToArray(); } /// /// Attaches the studios. /// /// The dto. /// The item. private void AttachStudios(BaseItemDto dto, BaseItem item) { dto.Studios = item.Studios .Where(i => !string.IsNullOrEmpty(i)) .Select(i => new NameGuidPair { Name = i, Id = _libraryManager.GetStudioId(i) }) .ToArray(); } private void AttachGenreItems(BaseItemDto dto, BaseItem item) { dto.GenreItems = item.Genres .Where(i => !string.IsNullOrEmpty(i)) .Select(i => new NameGuidPair { Name = i, Id = GetGenreId(i, item) }) .ToArray(); } private Guid GetGenreId(string name, BaseItem owner) { if (owner is IHasMusicGenres) { return _libraryManager.GetMusicGenreId(name); } return _libraryManager.GetGenreId(name); } private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0) { var image = item.GetImageInfo(imageType, imageIndex); if (image is not null) { return GetTagAndFillBlurhash(dto, item, image); } return null; } private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image) { var tag = GetImageCacheTag(item, image); if (!string.IsNullOrEmpty(image.BlurHash)) { dto.ImageBlurHashes ??= new Dictionary>(); if (!dto.ImageBlurHashes.ContainsKey(image.Type)) { dto.ImageBlurHashes[image.Type] = new Dictionary(); } dto.ImageBlurHashes[image.Type][tag] = image.BlurHash; } return tag; } private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit) { return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList()); } private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List images) { var tags = GetImageTags(item, images); var hashes = new Dictionary(); for (int i = 0; i < images.Count; i++) { var img = images[i]; if (!string.IsNullOrEmpty(img.BlurHash)) { var tag = tags[i]; hashes[tag] = img.BlurHash; } } if (hashes.Count > 0) { dto.ImageBlurHashes ??= new Dictionary>(); dto.ImageBlurHashes[imageType] = hashes; } return tags; } /// /// Sets simple property values on a DTOBaseItem. /// /// The dto. /// The item. /// The owner. /// The options. private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options) { if (options.ContainsField(ItemFields.DateCreated)) { dto.DateCreated = item.DateCreated; } if (options.ContainsField(ItemFields.Settings)) { dto.LockedFields = item.LockedFields; dto.LockData = item.IsLocked; dto.ForcedSortName = item.ForcedSortName; } dto.Container = item.Container; dto.EndDate = item.EndDate; if (options.ContainsField(ItemFields.ExternalUrls)) { dto.ExternalUrls = _providerManager.GetExternalUrls(item).ToArray(); } if (options.ContainsField(ItemFields.Tags)) { dto.Tags = item.Tags; } if (item is IHasAspectRatio hasAspectRatio) { dto.AspectRatio = hasAspectRatio.AspectRatio; } dto.ImageBlurHashes = new Dictionary>(); var backdropLimit = options.GetImageLimit(ImageType.Backdrop); if (backdropLimit > 0) { dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit); } if (options.ContainsField(ItemFields.Genres)) { dto.Genres = item.Genres; AttachGenreItems(dto, item); } if (options.EnableImages) { dto.ImageTags = new Dictionary(); // Prevent implicitly captured closure var currentItem = item; foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))) { if (options.GetImageLimit(image.Type) > 0) { var tag = GetTagAndFillBlurhash(dto, item, image); if (tag is not null) { dto.ImageTags[image.Type] = tag; } } } } dto.Id = item.Id; dto.IndexNumber = item.IndexNumber; dto.ParentIndexNumber = item.ParentIndexNumber; if (item.IsFolder) { dto.IsFolder = true; } else if (item is IHasMediaSources) { dto.IsFolder = false; } dto.MediaType = item.MediaType; if (item is not LiveTvProgram) { dto.LocationType = item.LocationType; } dto.Audio = item.Audio; if (options.ContainsField(ItemFields.Settings)) { dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; } dto.CriticRating = item.CriticRating; if (item is IHasDisplayOrder hasDisplayOrder) { dto.DisplayOrder = hasDisplayOrder.DisplayOrder; } if (item is IHasCollectionType hasCollectionType) { dto.CollectionType = hasCollectionType.CollectionType; } if (options.ContainsField(ItemFields.RemoteTrailers)) { dto.RemoteTrailers = item.RemoteTrailers; } dto.Name = item.Name; dto.OfficialRating = item.OfficialRating; if (options.ContainsField(ItemFields.Overview)) { dto.Overview = item.Overview; } if (options.ContainsField(ItemFields.OriginalTitle)) { dto.OriginalTitle = item.OriginalTitle; } if (options.ContainsField(ItemFields.ParentId)) { dto.ParentId = item.DisplayParentId; } AddInheritedImages(dto, item, options, owner); if (options.ContainsField(ItemFields.Path)) { dto.Path = GetMappedPath(item, owner); } if (options.ContainsField(ItemFields.EnableMediaSourceDisplay)) { dto.EnableMediaSourceDisplay = item.EnableMediaSourceDisplay; } dto.PremiereDate = item.PremiereDate; dto.ProductionYear = item.ProductionYear; if (options.ContainsField(ItemFields.ProviderIds)) { dto.ProviderIds = item.ProviderIds; } dto.RunTimeTicks = item.RunTimeTicks; if (options.ContainsField(ItemFields.SortName)) { dto.SortName = item.SortName; } if (options.ContainsField(ItemFields.CustomRating)) { dto.CustomRating = item.CustomRating; } if (options.ContainsField(ItemFields.Taglines)) { if (!string.IsNullOrEmpty(item.Tagline)) { dto.Taglines = new string[] { item.Tagline }; } dto.Taglines ??= Array.Empty(); } dto.Type = item.GetBaseItemKind(); if ((item.CommunityRating ?? 0) > 0) { dto.CommunityRating = item.CommunityRating; } if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder) { dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder; } // Add audio info if (item is Audio audio) { dto.Album = audio.Album; if (audio.ExtraType.HasValue) { dto.ExtraType = audio.ExtraType.Value.ToString(); } var albumParent = audio.AlbumEntity; if (albumParent is not null) { dto.AlbumId = albumParent.Id; dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary); } // if (options.ContainsField(ItemFields.MediaSourceCount)) // { // Songs always have one // } } if (item is IHasArtist hasArtist) { dto.Artists = hasArtist.Artists; // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery // { // EnableTotalRecordCount = false, // ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) } // }); // dto.ArtistItems = artistItems.Items // .Select(i => // { // var artist = i.Item1; // return new NameIdPair // { // Name = artist.Name, // Id = artist.Id.ToString("N", CultureInfo.InvariantCulture) // }; // }) // .ToList(); // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); dto.ArtistItems = hasArtist.Artists // .Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it if (string.IsNullOrEmpty(i)) { return null; } var artist = _libraryManager.GetArtist(i, new DtoOptions(false) { EnableImages = false }); if (artist is not null) { return new NameGuidPair { Name = artist.Name, Id = artist.Id }; } return null; }).Where(i => i is not null).ToArray(); } if (item is IHasAlbumArtist hasAlbumArtist) { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery // { // EnableTotalRecordCount = false, // ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) } // }); // dto.AlbumArtists = artistItems.Items // .Select(i => // { // var artist = i.Item1; // return new NameIdPair // { // Name = artist.Name, // Id = artist.Id.ToString("N", CultureInfo.InvariantCulture) // }; // }) // .ToList(); dto.AlbumArtists = hasAlbumArtist.AlbumArtists // .Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it if (string.IsNullOrEmpty(i)) { return null; } var artist = _libraryManager.GetArtist(i, new DtoOptions(false) { EnableImages = false }); if (artist is not null) { return new NameGuidPair { Name = artist.Name, Id = artist.Id }; } return null; }).Where(i => i is not null).ToArray(); } // Add video info if (item is Video video) { dto.VideoType = video.VideoType; dto.Video3DFormat = video.Video3DFormat; dto.IsoType = video.IsoType; if (video.HasSubtitles) { dto.HasSubtitles = video.HasSubtitles; } if (video.AdditionalParts.Length != 0) { dto.PartCount = video.AdditionalParts.Length + 1; } if (options.ContainsField(ItemFields.MediaSourceCount)) { var mediaSourceCount = video.MediaSourceCount; if (mediaSourceCount != 1) { dto.MediaSourceCount = mediaSourceCount; } } if (options.ContainsField(ItemFields.Chapters)) { dto.Chapters = _itemRepo.GetChapters(item); } if (video.ExtraType.HasValue) { dto.ExtraType = video.ExtraType.Value.ToString(); } } if (options.ContainsField(ItemFields.MediaStreams)) { // Add VideoInfo if (item is IHasMediaSources) { MediaStream[] mediaStreams; if (dto.MediaSources is not null && dto.MediaSources.Length > 0) { if (item.SourceType == SourceType.Channel) { mediaStreams = dto.MediaSources[0].MediaStreams.ToArray(); } else { string id = item.Id.ToString("N", CultureInfo.InvariantCulture); mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => i.MediaStreams) .ToArray(); } } else { mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray(); } dto.MediaStreams = mediaStreams; } } BaseItem[] allExtras = null; if (options.ContainsField(ItemFields.SpecialFeatureCount)) { allExtras = item.GetExtras().ToArray(); dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value)); } if (options.ContainsField(ItemFields.LocalTrailerCount)) { if (item is IHasTrailers hasTrailers) { dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count; } else { dto.LocalTrailerCount = (allExtras ?? item.GetExtras()).Count(i => i.ExtraType == ExtraType.Trailer); } } // Add EpisodeInfo if (item is Episode episode) { dto.IndexNumberEnd = episode.IndexNumberEnd; dto.SeriesName = episode.SeriesName; if (options.ContainsField(ItemFields.SpecialEpisodeNumbers)) { dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber; dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber; dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber; } dto.SeasonName = episode.SeasonName; dto.SeasonId = episode.SeasonId; dto.SeriesId = episode.SeriesId; Series episodeSeries = null; // this block will add the series poster for episodes without a poster // TODO maybe remove the if statement entirely // if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { episodeSeries ??= episode.Series; if (episodeSeries is not null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary)) { AttachPrimaryImageAspectRatio(dto, episodeSeries); } } } if (options.ContainsField(ItemFields.SeriesStudio)) { episodeSeries ??= episode.Series; if (episodeSeries is not null) { dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault(); } } } // Add SeriesInfo if (item is Series series) { dto.AirDays = series.AirDays; dto.AirTime = series.AirTime; dto.Status = series.Status?.ToString(); } // Add SeasonInfo if (item is Season season) { dto.SeriesName = season.SeriesName; dto.SeriesId = season.SeriesId; series = null; if (options.ContainsField(ItemFields.SeriesStudio)) { series ??= season.Series; if (series is not null) { dto.SeriesStudio = series.Studios.FirstOrDefault(); } } // this block will add the series poster for seasons without a poster // TODO maybe remove the if statement entirely // if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { series ??= season.Series; if (series is not null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary)) { AttachPrimaryImageAspectRatio(dto, series); } } } } if (item is MusicVideo musicVideo) { SetMusicVideoProperties(dto, musicVideo); } if (item is Book book) { SetBookProperties(dto, book); } if (options.ContainsField(ItemFields.ProductionLocations)) { if (item.ProductionLocations.Length > 0 || item is Movie) { dto.ProductionLocations = item.ProductionLocations; } } if (options.ContainsField(ItemFields.Width)) { var width = item.Width; if (width > 0) { dto.Width = width; } } if (options.ContainsField(ItemFields.Height)) { var height = item.Height; if (height > 0) { dto.Height = height; } } if (options.ContainsField(ItemFields.IsHD)) { // Compatibility if (item.IsHD) { dto.IsHD = true; } } if (item is Photo photo) { SetPhotoProperties(dto, photo); } dto.ChannelId = item.ChannelId; if (item.SourceType == SourceType.Channel) { var channel = _libraryManager.GetItemById(item.ChannelId); if (channel is not null) { dto.ChannelName = channel.Name; } } } private BaseItem GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem) { if (currentItem is MusicAlbum musicAlbum) { var artist = musicAlbum.GetMusicArtist(new DtoOptions(false)); if (artist is not null) { return artist; } } var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent(); if (parent is null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel) { parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault(); } return parent; } private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner) { if (!item.SupportsInheritedParentImages) { return; } var logoLimit = options.GetImageLimit(ImageType.Logo); var artLimit = options.GetImageLimit(ImageType.Art); var thumbLimit = options.GetImageLimit(ImageType.Thumb); var backdropLimit = options.GetImageLimit(ImageType.Backdrop); // For now. Emby apps are not using this artLimit = 0; if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0) { return; } BaseItem parent = null; var isFirst = true; var imageTags = dto.ImageTags; while ((!(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) { parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent; if (parent is null) { break; } var allImages = parent.ImageInfos; if (logoLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo); if (image is not null) { dto.ParentLogoItemId = parent.Id; dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image); } } if (artLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art); if (image is not null) { dto.ParentArtItemId = parent.Id; dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image); } } if (thumbLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); if (image is not null) { dto.ParentThumbItemId = parent.Id; dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image); } } if (backdropLimit > 0 && !((dto.BackdropImageTags is not null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags is not null && dto.ParentBackdropImageTags.Length > 0))) { var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList(); if (images.Count > 0) { dto.ParentBackdropItemId = parent.Id; dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images); } } isFirst = false; if (!parent.SupportsInheritedParentImages) { break; } parent = GetImageDisplayParent(parent, item); } } private string GetMappedPath(BaseItem item, BaseItem ownerItem) { var path = item.Path; if (item.IsFileProtocol) { path = _libraryManager.GetPathAfterNetworkSubstitution(path, ownerItem ?? item); } return path; } /// /// Attaches the primary image aspect ratio. /// /// The dto. /// The item. public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item) { dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item); } public double? GetPrimaryImageAspectRatio(BaseItem item) { var imageInfo = item.GetImageInfo(ImageType.Primary, 0); if (imageInfo is null) { return null; } if (!imageInfo.IsLocalFile) { return item.GetDefaultPrimaryImageAspectRatio(); } try { var size = _imageProcessor.GetImageDimensions(item, imageInfo); var width = size.Width; var height = size.Height; if (width > 0 && height > 0) { return (double)width / height; } } catch (Exception ex) { _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path); } return item.GetDefaultPrimaryImageAspectRatio(); } } }