#pragma warning disable RS0030 // Do not use banned APIs using System; using System.Collections.Concurrent; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity; namespace Jellyfin.Server.Implementations.Item; /// /// Handles mapping between BaseItemEntity (database) and BaseItemDto (domain) objects. /// internal static class BaseItemMapper { /// /// This holds all the types in the running assemblies /// so that we can de-serialize properly when we don't have strong types. /// private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); /// /// Maps a Entity to the DTO. /// /// The entity. /// The dto base instance. /// The Application server Host. /// The dto to map. public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost) { dto.Id = entity.Id; dto.ParentId = entity.ParentId.GetValueOrDefault(); dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path; dto.EndDate = entity.EndDate; dto.CommunityRating = entity.CommunityRating; dto.CustomRating = entity.CustomRating; dto.IndexNumber = entity.IndexNumber; dto.IsLocked = entity.IsLocked; dto.Name = entity.Name; dto.OfficialRating = entity.OfficialRating; dto.Overview = entity.Overview; dto.ParentIndexNumber = entity.ParentIndexNumber; dto.PremiereDate = entity.PremiereDate; dto.ProductionYear = entity.ProductionYear; dto.SortName = entity.SortName; dto.ForcedSortName = entity.ForcedSortName; dto.RunTimeTicks = entity.RunTimeTicks; dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage; dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; dto.IsInMixedFolder = entity.IsInMixedFolder; dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue; dto.CriticRating = entity.CriticRating; dto.PresentationUniqueKey = entity.PresentationUniqueKey; dto.OriginalTitle = entity.OriginalTitle; dto.Album = entity.Album; dto.LUFS = entity.LUFS; dto.NormalizationGain = entity.NormalizationGain; dto.IsVirtualItem = entity.IsVirtualItem; dto.ExternalSeriesId = entity.ExternalSeriesId; dto.Tagline = entity.Tagline; dto.TotalBitrate = entity.TotalBitrate; dto.ExternalId = entity.ExternalId; dto.Size = entity.Size; dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|'); dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); dto.ChannelId = entity.ChannelId ?? Guid.Empty; dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); dto.OwnerId = entity.OwnerId ?? Guid.Empty; dto.Width = entity.Width.GetValueOrDefault(); dto.Height = entity.Height.GetValueOrDefault(); dto.UserData = entity.UserData; if (entity.Provider is not null) { dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); } if (entity.ExtraType is not null) { dto.ExtraType = (ExtraType)entity.ExtraType; } if (entity.LockedFields is not null) { dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? []; } if (entity.Audio is not null) { dto.Audio = (ProgramAudio)entity.Audio; } dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? []; dto.Studios = entity.Studios?.Split('|') ?? []; dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|'); if (dto is IHasProgramAttributes hasProgramAttributes) { hasProgramAttributes.IsMovie = entity.IsMovie; hasProgramAttributes.IsSeries = entity.IsSeries; hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle; hasProgramAttributes.IsRepeat = entity.IsRepeat; } if (dto is LiveTvChannel liveTvChannel) { liveTvChannel.ServiceName = entity.ExternalServiceId; } if (dto is Trailer trailer) { trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? []; } if (dto is Video video) { video.PrimaryVersionId = entity.PrimaryVersionId; } if (dto is IHasSeries hasSeriesName) { hasSeriesName.SeriesName = entity.SeriesName; hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault(); hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey; } if (dto is Episode episode) { episode.SeasonName = entity.SeasonName; episode.SeasonId = entity.SeasonId.GetValueOrDefault(); } if (dto is IHasArtist hasArtists) { hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? []; } if (dto is IHasAlbumArtist hasAlbumArtists) { hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? []; } if (dto is LiveTvProgram program) { program.ShowId = entity.ShowId; } if (entity.Images is not null) { dto.ImageInfos = entity.Images.Select(e => MapImageFromEntity(e, appHost)).ToArray(); } // dto.Type = entity.Type; // dto.Data = entity.Data; // dto.MediaType = Enum.TryParse(entity.MediaType); if (dto is IHasStartDate hasStartDate) { hasStartDate.StartDate = entity.StartDate.GetValueOrDefault(); } // Fields that are present in the DB but are never actually used // dto.UnratedType = entity.UnratedType; // dto.TopParentId = entity.TopParentId; // dto.CleanName = entity.CleanName; // dto.UserDataKey = entity.UserDataKey; if (dto is Folder folder) { folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); if (entity.LinkedChildEntities is not null && entity.LinkedChildEntities.Count > 0) { folder.LinkedChildren = entity.LinkedChildEntities .OrderBy(e => e.SortOrder) .Select(e => new LinkedChild { ItemId = e.ChildId, Type = (MediaBrowser.Controller.Entities.LinkedChildType)e.ChildType }) .ToArray(); } } return dto; } /// /// Maps a DTO to a database entity. /// /// The DTO. /// The application host for path resolution. /// The database entity. public static BaseItemEntity Map(BaseItemDto dto, IServerApplicationHost appHost) { var dtoType = dto.GetType(); var entity = new BaseItemEntity() { Type = dtoType.ToString(), Id = dto.Id }; if (TypeRequiresDeserialization(dtoType)) { entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options); } entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null; entity.Path = GetPathToSave(dto.Path, appHost); entity.EndDate = dto.EndDate; entity.CommunityRating = dto.CommunityRating; entity.CustomRating = dto.CustomRating; entity.IndexNumber = dto.IndexNumber; entity.IsLocked = dto.IsLocked; entity.Name = dto.Name; entity.CleanName = dto.Name.GetCleanValue(); entity.OfficialRating = dto.OfficialRating; entity.Overview = dto.Overview; entity.ParentIndexNumber = dto.ParentIndexNumber; entity.PremiereDate = dto.PremiereDate; entity.ProductionYear = dto.ProductionYear; entity.SortName = dto.SortName; entity.ForcedSortName = dto.ForcedSortName; entity.RunTimeTicks = dto.RunTimeTicks; entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage; entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; entity.IsInMixedFolder = dto.IsInMixedFolder; entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue; entity.CriticRating = dto.CriticRating; entity.PresentationUniqueKey = dto.PresentationUniqueKey; entity.OriginalTitle = dto.OriginalTitle; entity.Album = dto.Album; entity.LUFS = dto.LUFS; entity.NormalizationGain = dto.NormalizationGain; entity.IsVirtualItem = dto.IsVirtualItem; entity.ExternalSeriesId = dto.ExternalSeriesId; entity.Tagline = dto.Tagline; entity.TotalBitrate = dto.TotalBitrate; entity.ExternalId = dto.ExternalId; entity.Size = dto.Size; entity.Genres = string.Join('|', dto.Genres); entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated; entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified; entity.ChannelId = dto.ChannelId; entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed; entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved; entity.OwnerId = dto.OwnerId == Guid.Empty ? null : dto.OwnerId; entity.Width = dto.Width; entity.Height = dto.Height; entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider() { Item = entity, ProviderId = e.Key, ProviderValue = e.Value }).ToList(); if (dto.Audio.HasValue) { entity.Audio = (ProgramAudioEntity)dto.Audio; } if (dto.ExtraType.HasValue) { entity.ExtraType = (BaseItemExtraType)dto.ExtraType; } entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null; entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null; entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null; entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields .Select(e => new BaseItemMetadataField() { Id = (int)e, Item = entity, ItemId = entity.Id }) .ToArray() : null; if (dto is IHasProgramAttributes hasProgramAttributes) { entity.IsMovie = hasProgramAttributes.IsMovie; entity.IsSeries = hasProgramAttributes.IsSeries; entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle; entity.IsRepeat = hasProgramAttributes.IsRepeat; } if (dto is LiveTvChannel liveTvChannel) { entity.ExternalServiceId = liveTvChannel.ServiceName; } if (dto is Video video) { entity.PrimaryVersionId = video.PrimaryVersionId; } if (dto is IHasSeries hasSeriesName) { entity.SeriesName = hasSeriesName.SeriesName; entity.SeriesId = hasSeriesName.SeriesId; entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey; } if (dto is Episode episode) { entity.SeasonName = episode.SeasonName; entity.SeasonId = episode.SeasonId; } if (dto is IHasArtist hasArtists) { entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null; } if (dto is IHasAlbumArtist hasAlbumArtists) { entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null; } if (dto is LiveTvProgram program) { entity.ShowId = program.ShowId; } if (dto.ImageInfos is not null) { entity.Images = dto.ImageInfos.Select(f => MapImageToEntity(dto.Id, f)).ToArray(); } if (dto is Trailer trailer) { entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType() { Id = (int)e, Item = entity, ItemId = entity.Id }).ToArray() ?? []; } // dto.Type = entity.Type; // dto.Data = entity.Data; entity.MediaType = dto.MediaType.ToString(); if (dto is IHasStartDate hasStartDate) { entity.StartDate = hasStartDate.StartDate; } entity.UnratedType = dto.GetBlockUnratedType().ToString(); // Fields that are present in the DB but are never actually used // dto.UserDataKey = entity.UserDataKey; if (dto is Folder folder) { entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded; entity.IsFolder = folder.IsFolder; } return entity; } /// /// Maps a database image entity to a domain image info. /// /// The database image entity. /// The application host. /// The mapped image info. public static ItemImageInfo MapImageFromEntity(BaseItemImageInfo e, IServerApplicationHost? appHost) { return new ItemImageInfo() { Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path, BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash), DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc), Height = e.Height, Width = e.Width, Type = (ImageType)e.ImageType }; } /// /// Maps a domain image info to a database image entity. /// /// The parent item ID. /// The image info to map. /// The mapped database entity. public static BaseItemImageInfo MapImageToEntity(Guid baseItemId, ItemImageInfo e) { return new BaseItemImageInfo() { ItemId = baseItemId, Id = Guid.NewGuid(), Path = e.Path, Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash), DateModified = e.DateModified, Height = e.Height, Width = e.Width, ImageType = (ImageInfoImageType)e.Type, Item = null! }; } /// /// Gets the type from a type name string. /// /// The type name. /// The resolved type, or null. public static Type? GetType(string typeName) { ArgumentException.ThrowIfNullOrEmpty(typeName); return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(k)) .FirstOrDefault(t => t is not null)); } /// /// Checks whether a type requires JSON deserialization. /// /// The type to check. /// True if the type requires deserialization. public static bool TypeRequiresDeserialization(Type type) { return type.GetCustomAttribute() == null; } /// /// Deserializes a BaseItemEntity and sets all properties. /// /// The DB entity. /// Logger. /// The application server Host. /// If only mapping should be processed. /// A mapped BaseItem, or null if the item type is unknown. public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false) { var type = GetType(baseItemEntity.Type); if (type is null) { logger.LogWarning( "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database corruption.", baseItemEntity.Id, baseItemEntity.Type); return null; } BaseItemDto? dto = null; if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization) { try { dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto; } catch (JsonException ex) { logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data); } } if (dto is null) { dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type."); } return Map(baseItemEntity, dto, appHost); } private static string? GetPathToSave(string path, IServerApplicationHost appHost) { if (path is null) { return null; } return appHost.ReverseVirtualPath(path); } }