< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.BaseItemMapper
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
Line coverage
71%
Covered lines: 181
Uncovered lines: 72
Coverable lines: 253
Total lines: 489
Line coverage: 71.5%
Branch coverage
54%
Covered branches: 92
Total branches: 168
Branch coverage: 54.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/4/2026 - 12:15:16 AM Line coverage: 71.5% (181/253) Branch coverage: 54.7% (92/168) Total lines: 489 5/4/2026 - 12:15:16 AM Line coverage: 71.5% (181/253) Branch coverage: 54.7% (92/168) Total lines: 489

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Map(...)51.21%2208272.63%
Map(...)54.83%966279.27%
MapImageFromEntity(...)0%7280%
MapImageToEntity(...)0%620%
GetType(...)100%11100%
TypeRequiresDeserialization(...)100%11100%
DeserializeBaseItem(...)83.33%161270.58%
GetPathToSave(...)50%2266.66%

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Linq;
 6using System.Reflection;
 7using System.Text;
 8using System.Text.Json;
 9using Jellyfin.Database.Implementations.Entities;
 10using Jellyfin.Extensions;
 11using Jellyfin.Extensions.Json;
 12using MediaBrowser.Common;
 13using MediaBrowser.Controller;
 14using MediaBrowser.Controller.Entities;
 15using MediaBrowser.Controller.Entities.Audio;
 16using MediaBrowser.Controller.Entities.TV;
 17using MediaBrowser.Controller.LiveTv;
 18using MediaBrowser.Model.Entities;
 19using MediaBrowser.Model.LiveTv;
 20using Microsoft.Extensions.Logging;
 21using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 22using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 23
 24namespace Jellyfin.Server.Implementations.Item;
 25
 26/// <summary>
 27/// Handles mapping between BaseItemEntity (database) and BaseItemDto (domain) objects.
 28/// </summary>
 29internal static class BaseItemMapper
 30{
 31    /// <summary>
 32    /// This holds all the types in the running assemblies
 33    /// so that we can de-serialize properly when we don't have strong types.
 34    /// </summary>
 235    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 36
 37    /// <summary>
 38    /// Maps a Entity to the DTO.
 39    /// </summary>
 40    /// <param name="entity">The entity.</param>
 41    /// <param name="dto">The dto base instance.</param>
 42    /// <param name="appHost">The Application server Host.</param>
 43    /// <returns>The dto to map.</returns>
 44    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
 45    {
 7346        dto.Id = entity.Id;
 7347        dto.ParentId = entity.ParentId.GetValueOrDefault();
 7348        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 7349        dto.EndDate = entity.EndDate;
 7350        dto.CommunityRating = entity.CommunityRating;
 7351        dto.CustomRating = entity.CustomRating;
 7352        dto.IndexNumber = entity.IndexNumber;
 7353        dto.IsLocked = entity.IsLocked;
 7354        dto.Name = entity.Name;
 7355        dto.OfficialRating = entity.OfficialRating;
 7356        dto.Overview = entity.Overview;
 7357        dto.ParentIndexNumber = entity.ParentIndexNumber;
 7358        dto.PremiereDate = entity.PremiereDate;
 7359        dto.ProductionYear = entity.ProductionYear;
 7360        dto.SortName = entity.SortName;
 7361        dto.ForcedSortName = entity.ForcedSortName;
 7362        dto.RunTimeTicks = entity.RunTimeTicks;
 7363        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 7364        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 7365        dto.IsInMixedFolder = entity.IsInMixedFolder;
 7366        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 7367        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 7368        dto.CriticRating = entity.CriticRating;
 7369        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 7370        dto.OriginalTitle = entity.OriginalTitle;
 7371        dto.Album = entity.Album;
 7372        dto.LUFS = entity.LUFS;
 7373        dto.NormalizationGain = entity.NormalizationGain;
 7374        dto.IsVirtualItem = entity.IsVirtualItem;
 7375        dto.ExternalSeriesId = entity.ExternalSeriesId;
 7376        dto.Tagline = entity.Tagline;
 7377        dto.TotalBitrate = entity.TotalBitrate;
 7378        dto.ExternalId = entity.ExternalId;
 7379        dto.Size = entity.Size;
 7380        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 7381        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 7382        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 7383        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 7384        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 7385        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 7386        dto.OwnerId = entity.OwnerId ?? Guid.Empty;
 7387        dto.Width = entity.Width.GetValueOrDefault();
 7388        dto.Height = entity.Height.GetValueOrDefault();
 7389        dto.UserData = entity.UserData;
 90
 7391        if (entity.Provider is not null)
 92        {
 7293            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 94        }
 95
 7396        if (entity.ExtraType is not null)
 97        {
 098            dto.ExtraType = (ExtraType)entity.ExtraType;
 99        }
 100
 73101        if (entity.LockedFields is not null)
 102        {
 72103            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 104        }
 105
 73106        if (entity.Audio is not null)
 107        {
 0108            dto.Audio = (ProgramAudio)entity.Audio;
 109        }
 110
 73111        dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 73112        dto.Studios = entity.Studios?.Split('|') ?? [];
 73113        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 114
 73115        if (dto is IHasProgramAttributes hasProgramAttributes)
 116        {
 0117            hasProgramAttributes.IsMovie = entity.IsMovie;
 0118            hasProgramAttributes.IsSeries = entity.IsSeries;
 0119            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0120            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 121        }
 122
 73123        if (dto is LiveTvChannel liveTvChannel)
 124        {
 0125            liveTvChannel.ServiceName = entity.ExternalServiceId;
 126        }
 127
 73128        if (dto is Trailer trailer)
 129        {
 0130            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 131        }
 132
 73133        if (dto is Video video)
 134        {
 1135            video.PrimaryVersionId = entity.PrimaryVersionId;
 136        }
 137
 73138        if (dto is IHasSeries hasSeriesName)
 139        {
 0140            hasSeriesName.SeriesName = entity.SeriesName;
 0141            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0142            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 143        }
 144
 73145        if (dto is Episode episode)
 146        {
 0147            episode.SeasonName = entity.SeasonName;
 0148            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 149        }
 150
 73151        if (dto is IHasArtist hasArtists)
 152        {
 0153            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 154        }
 155
 73156        if (dto is IHasAlbumArtist hasAlbumArtists)
 157        {
 0158            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 159        }
 160
 73161        if (dto is LiveTvProgram program)
 162        {
 0163            program.ShowId = entity.ShowId;
 164        }
 165
 73166        if (entity.Images is not null)
 167        {
 72168            dto.ImageInfos = entity.Images.Select(e => MapImageFromEntity(e, appHost)).ToArray();
 169        }
 170
 73171        if (dto is IHasStartDate hasStartDate)
 172        {
 0173            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 174        }
 175
 176        // Fields that are present in the DB but are never actually used
 177        // dto.UnratedType = entity.UnratedType;
 178        // dto.TopParentId = entity.TopParentId;
 179        // dto.CleanName = entity.CleanName;
 180        // dto.UserDataKey = entity.UserDataKey;
 181
 73182        if (dto is Folder folder)
 183        {
 72184            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 72185            if (entity.LinkedChildEntities is not null && entity.LinkedChildEntities.Count > 0)
 186            {
 0187                folder.LinkedChildren = entity.LinkedChildEntities
 0188                    .OrderBy(e => e.SortOrder)
 0189                    .Select(e => new LinkedChild
 0190                    {
 0191                        ItemId = e.ChildId,
 0192                        Type = (MediaBrowser.Controller.Entities.LinkedChildType)e.ChildType
 0193                    })
 0194                    .ToArray();
 195            }
 196        }
 197
 73198        return dto;
 199    }
 200
 201    /// <summary>
 202    /// Maps a DTO to a database entity.
 203    /// </summary>
 204    /// <param name="dto">The DTO.</param>
 205    /// <param name="appHost">The application host for path resolution.</param>
 206    /// <returns>The database entity.</returns>
 207    public static BaseItemEntity Map(BaseItemDto dto, IServerApplicationHost appHost)
 208    {
 123209        var dtoType = dto.GetType();
 123210        var entity = new BaseItemEntity()
 123211        {
 123212            Type = dtoType.ToString(),
 123213            Id = dto.Id
 123214        };
 215
 123216        if (TypeRequiresDeserialization(dtoType))
 217        {
 102218            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 219        }
 220
 123221        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 123222        entity.Path = GetPathToSave(dto.Path, appHost);
 123223        entity.EndDate = dto.EndDate;
 123224        entity.CommunityRating = dto.CommunityRating;
 123225        entity.CustomRating = dto.CustomRating;
 123226        entity.IndexNumber = dto.IndexNumber;
 123227        entity.IsLocked = dto.IsLocked;
 123228        entity.Name = dto.Name;
 123229        entity.CleanName = dto.Name.GetCleanValue();
 123230        entity.OfficialRating = dto.OfficialRating;
 123231        entity.Overview = dto.Overview;
 123232        entity.ParentIndexNumber = dto.ParentIndexNumber;
 123233        entity.PremiereDate = dto.PremiereDate;
 123234        entity.ProductionYear = dto.ProductionYear;
 123235        entity.SortName = dto.SortName;
 123236        entity.ForcedSortName = dto.ForcedSortName;
 123237        entity.RunTimeTicks = dto.RunTimeTicks;
 123238        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 123239        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 123240        entity.IsInMixedFolder = dto.IsInMixedFolder;
 123241        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 123242        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 123243        entity.CriticRating = dto.CriticRating;
 123244        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 123245        entity.OriginalTitle = dto.OriginalTitle;
 123246        entity.Album = dto.Album;
 123247        entity.LUFS = dto.LUFS;
 123248        entity.NormalizationGain = dto.NormalizationGain;
 123249        entity.IsVirtualItem = dto.IsVirtualItem;
 123250        entity.ExternalSeriesId = dto.ExternalSeriesId;
 123251        entity.Tagline = dto.Tagline;
 123252        entity.TotalBitrate = dto.TotalBitrate;
 123253        entity.ExternalId = dto.ExternalId;
 123254        entity.Size = dto.Size;
 123255        entity.Genres = string.Join('|', dto.Genres.Distinct(StringComparer.OrdinalIgnoreCase));
 123256        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 123257        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 123258        entity.ChannelId = dto.ChannelId;
 123259        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 123260        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 123261        entity.OwnerId = dto.OwnerId == Guid.Empty ? null : dto.OwnerId;
 123262        entity.Width = dto.Width;
 123263        entity.Height = dto.Height;
 123264        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 123265        {
 123266            Item = entity,
 123267            ProviderId = e.Key,
 123268            ProviderValue = e.Value
 123269        }).ToList();
 270
 123271        if (dto.Audio.HasValue)
 272        {
 0273            entity.Audio = (ProgramAudioEntity)dto.Audio;
 274        }
 275
 123276        if (dto.ExtraType.HasValue)
 277        {
 0278            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 279        }
 280
 123281        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Wher
 123282        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios.Distinct(StringComparer.OrdinalIgnoreCas
 123283        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags.Distinct(StringComparer.OrdinalIgnoreCase)) : nul
 123284        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 123285            .Select(e => new BaseItemMetadataField()
 123286            {
 123287                Id = (int)e,
 123288                Item = entity,
 123289                ItemId = entity.Id
 123290            })
 123291            .ToArray() : null;
 292
 123293        if (dto is IHasProgramAttributes hasProgramAttributes)
 294        {
 0295            entity.IsMovie = hasProgramAttributes.IsMovie;
 0296            entity.IsSeries = hasProgramAttributes.IsSeries;
 0297            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 0298            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 299        }
 300
 123301        if (dto is LiveTvChannel liveTvChannel)
 302        {
 0303            entity.ExternalServiceId = liveTvChannel.ServiceName;
 304        }
 305
 123306        if (dto is Video video)
 307        {
 0308            entity.PrimaryVersionId = video.PrimaryVersionId;
 309        }
 310
 123311        if (dto is IHasSeries hasSeriesName)
 312        {
 0313            entity.SeriesName = hasSeriesName.SeriesName;
 0314            entity.SeriesId = hasSeriesName.SeriesId;
 0315            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 316        }
 317
 123318        if (dto is Episode episode)
 319        {
 0320            entity.SeasonName = episode.SeasonName;
 0321            entity.SeasonId = episode.SeasonId;
 322        }
 323
 123324        if (dto is IHasArtist hasArtists)
 325        {
 0326            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists.Distinct(StringCompare
 327        }
 328
 123329        if (dto is IHasAlbumArtist hasAlbumArtists)
 330        {
 0331            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 332        }
 333
 123334        if (dto is LiveTvProgram program)
 335        {
 0336            entity.ShowId = program.ShowId;
 337        }
 338
 123339        if (dto.ImageInfos is not null)
 340        {
 123341            entity.Images = dto.ImageInfos.Select(f => MapImageToEntity(dto.Id, f)).ToArray();
 342        }
 343
 123344        if (dto is Trailer trailer)
 345        {
 0346            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 0347            {
 0348                Id = (int)e,
 0349                Item = entity,
 0350                ItemId = entity.Id
 0351            }).ToArray() ?? [];
 352        }
 353
 123354        entity.MediaType = dto.MediaType.ToString();
 123355        if (dto is IHasStartDate hasStartDate)
 356        {
 0357            entity.StartDate = hasStartDate.StartDate;
 358        }
 359
 123360        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 361
 362        // Fields that are present in the DB but are never actually used
 363        // dto.UserDataKey = entity.UserDataKey;
 364
 123365        if (dto is Folder folder)
 366        {
 123367            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 123368            entity.IsFolder = folder.IsFolder;
 369        }
 370
 123371        return entity;
 372    }
 373
 374    /// <summary>
 375    /// Maps a database image entity to a domain image info.
 376    /// </summary>
 377    /// <param name="e">The database image entity.</param>
 378    /// <param name="appHost">The application host.</param>
 379    /// <returns>The mapped image info.</returns>
 380    public static ItemImageInfo MapImageFromEntity(BaseItemImageInfo e, IServerApplicationHost? appHost)
 381    {
 0382        return new ItemImageInfo()
 0383        {
 0384            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 0385            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 0386            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 0387            Height = e.Height,
 0388            Width = e.Width,
 0389            Type = (ImageType)e.ImageType
 0390        };
 391    }
 392
 393    /// <summary>
 394    /// Maps a domain image info to a database image entity.
 395    /// </summary>
 396    /// <param name="baseItemId">The parent item ID.</param>
 397    /// <param name="e">The image info to map.</param>
 398    /// <returns>The mapped database entity.</returns>
 399    public static BaseItemImageInfo MapImageToEntity(Guid baseItemId, ItemImageInfo e)
 400    {
 0401        return new BaseItemImageInfo()
 0402        {
 0403            ItemId = baseItemId,
 0404            Id = Guid.NewGuid(),
 0405            Path = e.Path,
 0406            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 0407            DateModified = e.DateModified,
 0408            Height = e.Height,
 0409            Width = e.Width,
 0410            ImageType = (ImageInfoImageType)e.Type,
 0411            Item = null!
 0412        };
 413    }
 414
 415    /// <summary>
 416    /// Gets the type from a type name string.
 417    /// </summary>
 418    /// <param name="typeName">The type name.</param>
 419    /// <returns>The resolved type, or null.</returns>
 420    public static Type? GetType(string typeName)
 421    {
 147422        ArgumentException.ThrowIfNullOrEmpty(typeName);
 423
 147424        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 147425            .Select(a => a.GetType(k))
 147426            .FirstOrDefault(t => t is not null));
 427    }
 428
 429    /// <summary>
 430    /// Checks whether a type requires JSON deserialization.
 431    /// </summary>
 432    /// <param name="type">The type to check.</param>
 433    /// <returns>True if the type requires deserialization.</returns>
 434    public static bool TypeRequiresDeserialization(Type type)
 435    {
 196436        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 437    }
 438
 439    /// <summary>
 440    /// Deserializes a BaseItemEntity and sets all properties.
 441    /// </summary>
 442    /// <param name="baseItemEntity">The DB entity.</param>
 443    /// <param name="logger">Logger.</param>
 444    /// <param name="appHost">The application server Host.</param>
 445    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 446    /// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
 447    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 448    {
 75449        var type = GetType(baseItemEntity.Type);
 75450        if (type is null)
 451        {
 2452            logger.LogWarning(
 2453                "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database c
 2454                baseItemEntity.Id,
 2455                baseItemEntity.Type);
 2456            return null;
 457        }
 458
 73459        BaseItemDto? dto = null;
 73460        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 461        {
 462            try
 463            {
 12464                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 12465            }
 0466            catch (JsonException ex)
 467            {
 0468                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 0469            }
 470        }
 471
 73472        if (dto is null)
 473        {
 61474            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 475        }
 476
 73477        return Map(baseItemEntity, dto, appHost);
 478    }
 479
 480    private static string? GetPathToSave(string path, IServerApplicationHost appHost)
 481    {
 123482        if (path is null)
 483        {
 10484            return null;
 485        }
 486
 113487        return appHost.ReverseVirtualPath(path);
 488    }
 489}