< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.LinkedChildrenService
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs
Line coverage
21%
Covered lines: 21
Uncovered lines: 78
Coverable lines: 99
Total lines: 202
Line coverage: 21.2%
Branch coverage
7%
Covered branches: 1
Total branches: 14
Branch coverage: 7.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/8/2026 - 12:15:13 AM Line coverage: 24.6% (19/77) Branch coverage: 20% (2/10) Total lines: 1625/11/2026 - 12:15:59 AM Line coverage: 26.5% (21/79) Branch coverage: 20% (2/10) Total lines: 1665/30/2026 - 12:15:32 AM Line coverage: 24.7% (21/85) Branch coverage: 16.6% (2/12) Total lines: 1778/2/2026 - 12:17:28 AM Line coverage: 23.8% (21/88) Branch coverage: 16.6% (2/12) Total lines: 1818/11/2026 - 12:17:00 AM Line coverage: 21.2% (21/99) Branch coverage: 14.2% (2/14) Total lines: 2008/15/2026 - 12:17:03 AM Line coverage: 21.2% (21/99) Branch coverage: 7.1% (1/14) Total lines: 202 5/8/2026 - 12:15:13 AM Line coverage: 24.6% (19/77) Branch coverage: 20% (2/10) Total lines: 1625/11/2026 - 12:15:59 AM Line coverage: 26.5% (21/79) Branch coverage: 20% (2/10) Total lines: 1665/30/2026 - 12:15:32 AM Line coverage: 24.7% (21/85) Branch coverage: 16.6% (2/12) Total lines: 1778/2/2026 - 12:17:28 AM Line coverage: 23.8% (21/88) Branch coverage: 16.6% (2/12) Total lines: 1818/11/2026 - 12:17:00 AM Line coverage: 21.2% (21/99) Branch coverage: 14.2% (2/14) Total lines: 2008/15/2026 - 12:17:03 AM Line coverage: 21.2% (21/99) Branch coverage: 7.1% (1/14) Total lines: 202

Coverage delta

Coverage delta 8 -8

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetLinkedChildrenIds(...)0%620%
GetItemIdsWithAlternateVersions(...)0%620%
FindArtists(...)25%4489.47%
GetManualLinkedParentIds(...)0%620%
RerouteLinkedChildren(...)0%620%
UpsertLinkedChild(...)0%620%

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1311 // Specify a culture or use an invariant version
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Linq;
 8using Jellyfin.Data.Enums;
 9using Jellyfin.Database.Implementations;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Controller.Entities.Audio;
 12using MediaBrowser.Controller.Persistence;
 13using Microsoft.EntityFrameworkCore;
 14using DbLinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType;
 15using LinkedChildType = MediaBrowser.Controller.Entities.LinkedChildType;
 16
 17namespace Jellyfin.Server.Implementations.Item;
 18
 19/// <summary>
 20/// Provides linked children query and manipulation operations.
 21/// </summary>
 22public class LinkedChildrenService : ILinkedChildrenService
 23{
 24    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 25    private readonly IItemTypeLookup _itemTypeLookup;
 26    private readonly IItemQueryHelpers _queryHelpers;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="LinkedChildrenService"/> class.
 30    /// </summary>
 31    /// <param name="dbProvider">The database context factory.</param>
 32    /// <param name="itemTypeLookup">The item type lookup.</param>
 33    /// <param name="queryHelpers">The shared query helpers.</param>
 34    public LinkedChildrenService(
 35        IDbContextFactory<JellyfinDbContext> dbProvider,
 36        IItemTypeLookup itemTypeLookup,
 37        IItemQueryHelpers queryHelpers)
 38    {
 2239        _dbProvider = dbProvider;
 2240        _itemTypeLookup = itemTypeLookup;
 2241        _queryHelpers = queryHelpers;
 2242    }
 43
 44    /// <inheritdoc/>
 45    public IReadOnlyList<Guid> GetLinkedChildrenIds(Guid parentId, int? childType = null)
 46    {
 047        using var dbContext = _dbProvider.CreateDbContext();
 48
 049        var query = dbContext.LinkedChildren
 050            .Where(lc => lc.ParentId.Equals(parentId));
 51
 052        if (childType.HasValue)
 53        {
 054            query = query.Where(lc => (int)lc.ChildType == childType.Value);
 55        }
 56
 057        return query
 058            .OrderBy(lc => lc.SortOrder)
 059            .Select(lc => lc.ChildId)
 060            .ToArray();
 061    }
 62
 63    /// <inheritdoc/>
 64    public IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds)
 65    {
 066        if (itemIds.Count == 0)
 67        {
 068            return new HashSet<Guid>();
 69        }
 70
 071        using var dbContext = _dbProvider.CreateDbContext();
 72
 073        return dbContext.LinkedChildren
 074            .Where(lc => lc.ChildType == DbLinkedChildType.LocalAlternateVersion
 075                || lc.ChildType == DbLinkedChildType.LinkedAlternateVersion)
 076            .WhereOneOrMany(itemIds, lc => lc.ParentId)
 077            .Select(lc => lc.ParentId)
 078            .Distinct()
 079            .ToHashSet();
 080    }
 81
 82    /// <inheritdoc/>
 83    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 84    {
 1885        using var dbContext = _dbProvider.CreateDbContext();
 86
 1887        var cleanNames = artistNames.Select(n => (Original: n, Clean: n.GetCleanValue())).ToArray();
 1888        var cleanValues = cleanNames.Select(x => x.Clean).ToArray();
 89
 1890        var artists = dbContext.BaseItems
 1891            .AsNoTracking()
 1892            .Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!)
 1893            .Where(e => cleanValues.Contains(e.CleanName))
 1894            .ToArray();
 95
 1896        var lookup = artists
 1897            .GroupBy(e => e.CleanName!)
 1898            .ToDictionary(
 1899                g => g.Key,
 18100                g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>
 101
 18102        var result = new Dictionary<string, MusicArtist[]>(cleanNames.Length);
 36103        foreach (var (original, clean) in cleanNames)
 104        {
 0105            if (lookup.TryGetValue(clean, out var artistArray))
 106            {
 0107                result[original] = artistArray;
 108            }
 109        }
 110
 18111        return result;
 18112    }
 113
 114    /// <inheritdoc/>
 115    public IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId, BaseItemKind? parentType = null)
 116    {
 0117        using var context = _dbProvider.CreateDbContext();
 118
 0119        var query = context.LinkedChildren
 0120            .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual);
 121
 0122        if (parentType.HasValue)
 123        {
 0124            var parentTypeName = _itemTypeLookup.BaseItemKindNames[parentType.Value];
 0125            query = query.Join(
 0126                context.BaseItems
 0127                    .Where(item => item.Type == parentTypeName),
 0128                lc => lc.ParentId,
 0129                item => item.Id,
 0130                (lc, _) => lc);
 131        }
 132
 0133        return query.Select(lc => lc.ParentId).Distinct().ToList();
 0134    }
 135
 136    /// <inheritdoc/>
 137    public IReadOnlyList<Guid> RerouteLinkedChildren(Guid fromChildId, Guid toChildId)
 138    {
 0139        using var context = _dbProvider.CreateDbContext();
 140
 0141        var affectedParentIds = context.LinkedChildren
 0142            .Where(lc => lc.ChildId == fromChildId && lc.ChildType == DbLinkedChildType.Manual)
 0143            .Select(lc => lc.ParentId)
 0144            .Distinct()
 0145            .ToList();
 146
 0147        if (affectedParentIds.Count == 0)
 148        {
 0149            return affectedParentIds;
 150        }
 151
 0152        var parentsWithTarget = context.LinkedChildren
 0153            .Where(lc => lc.ChildId == toChildId && lc.ChildType == DbLinkedChildType.Manual)
 0154            .Select(lc => lc.ParentId)
 0155            .ToHashSet();
 156
 0157        context.LinkedChildren
 0158            .Where(lc => lc.ChildId == fromChildId
 0159                && lc.ChildType == DbLinkedChildType.Manual
 0160                && !parentsWithTarget.Contains(lc.ParentId))
 0161            .ExecuteUpdate(s => s.SetProperty(e => e.ChildId, toChildId));
 162
 0163        context.LinkedChildren
 0164            .Where(lc => lc.ChildId == fromChildId
 0165                && lc.ChildType == DbLinkedChildType.Manual
 0166                && parentsWithTarget.Contains(lc.ParentId))
 0167            .ExecuteDelete();
 168
 0169        return affectedParentIds;
 0170    }
 171
 172    /// <inheritdoc/>
 173    public void UpsertLinkedChild(Guid parentId, Guid childId, LinkedChildType childType)
 174    {
 0175        using var context = _dbProvider.CreateDbContext();
 176
 0177        var dbChildType = (DbLinkedChildType)childType;
 0178        var existingLink = context.LinkedChildren
 0179            .FirstOrDefault(lc => lc.ParentId == parentId && lc.ChildId == childId);
 180
 0181        if (existingLink is null)
 182        {
 0183            var nextSortOrder = (context.LinkedChildren
 0184                .Where(lc => lc.ParentId == parentId)
 0185                .Max(lc => (int?)lc.SortOrder) ?? -1) + 1;
 186
 0187            context.LinkedChildren.Add(new Jellyfin.Database.Implementations.Entities.LinkedChildEntity
 0188            {
 0189                ParentId = parentId,
 0190                ChildId = childId,
 0191                ChildType = dbChildType,
 0192                SortOrder = nextSortOrder
 0193            });
 194        }
 195        else
 196        {
 0197            existingLink.ChildType = dbChildType;
 198        }
 199
 0200        context.SaveChanges();
 0201    }
 202}