< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.PeopleRepository
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
Line coverage
0%
Covered lines: 1
Uncovered lines: 132
Coverable lines: 133
Total lines: 288
Line coverage: 0.7%
Branch coverage
0%
Covered branches: 0
Total branches: 70
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 0.8% (1/114) Branch coverage: 0% (0/50) Total lines: 2532/6/2026 - 12:13:21 AM Line coverage: 0.8% (1/115) Branch coverage: 0% (0/54) Total lines: 2544/6/2026 - 12:13:55 AM Line coverage: 0.8% (1/119) Branch coverage: 0% (0/60) Total lines: 2634/14/2026 - 12:13:23 AM Line coverage: 0.8% (1/125) Branch coverage: 0% (0/66) Total lines: 2785/5/2026 - 12:15:44 AM Line coverage: 0.7% (1/133) Branch coverage: 0% (0/70) Total lines: 288 1/23/2026 - 12:11:06 AM Line coverage: 0.8% (1/114) Branch coverage: 0% (0/50) Total lines: 2532/6/2026 - 12:13:21 AM Line coverage: 0.8% (1/115) Branch coverage: 0% (0/54) Total lines: 2544/6/2026 - 12:13:55 AM Line coverage: 0.8% (1/119) Branch coverage: 0% (0/60) Total lines: 2634/14/2026 - 12:13:23 AM Line coverage: 0.8% (1/125) Branch coverage: 0% (0/66) Total lines: 2785/5/2026 - 12:15:44 AM Line coverage: 0.7% (1/133) Branch coverage: 0% (0/70) Total lines: 288

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPeople(...)0%7280%
GetPeopleNames(...)0%4260%
UpdatePeople(...)0%210140%
Map(...)0%7280%
Map(...)100%210%
TranslateQuery(...)0%702260%
IsAlphaNumeric(...)0%7280%
IsValidPersonType(...)100%210%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using Jellyfin.Data.Enums;
 5using Jellyfin.Database.Implementations;
 6using Jellyfin.Database.Implementations.Entities;
 7using Jellyfin.Extensions;
 8using MediaBrowser.Controller.Entities;
 9using MediaBrowser.Controller.Persistence;
 10using MediaBrowser.Model.Querying;
 11using Microsoft.EntityFrameworkCore;
 12
 13namespace Jellyfin.Server.Implementations.Item;
 14#pragma warning disable RS0030 // Do not use banned APIs
 15#pragma warning disable CA1304 // Specify CultureInfo
 16#pragma warning disable CA1311 // Specify a culture or use an invariant version
 17#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 18
 19/// <summary>
 20/// Manager for handling people.
 21/// </summary>
 22/// <param name="dbProvider">Efcore Factory.</param>
 23/// <param name="itemTypeLookup">Items lookup service.</param>
 24/// <remarks>
 25/// Initializes a new instance of the <see cref="PeopleRepository"/> class.
 26/// </remarks>
 27public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IItemTypeLookup itemTypeLookup) : IPeople
 28{
 2129    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider = dbProvider;
 30
 31    /// <inheritdoc/>
 32    public QueryResult<PersonInfo> GetPeople(InternalPeopleQuery filter)
 33    {
 034        using var context = _dbProvider.CreateDbContext();
 035        var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
 36
 37        // Include PeopleBaseItemMap
 038        if (!filter.ItemId.IsEmpty())
 39        {
 040            dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId))
 041                .OrderBy(e => e.BaseItems!.First(e => e.ItemId == filter.ItemId).ListOrder)
 042                .ThenBy(e => e.PersonType)
 043                .ThenBy(e => e.Name);
 44        }
 45        else
 46        {
 047            dbQuery = dbQuery.OrderBy(e => e.Name);
 48        }
 49
 050        var count = dbQuery.Count();
 051        if (filter.StartIndex.HasValue && filter.StartIndex > 0)
 52        {
 053            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 54        }
 55
 056        if (filter.Limit > 0)
 57        {
 058            dbQuery = dbQuery.Take(filter.Limit);
 59        }
 60
 061        return new QueryResult<PersonInfo>
 062        {
 063            StartIndex = filter.StartIndex ?? 0,
 064            TotalRecordCount = count,
 065            Items = dbQuery.AsEnumerable().Select(Map).ToArray(),
 066        };
 067    }
 68
 69    /// <inheritdoc/>
 70    public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter)
 71    {
 072        using var context = _dbProvider.CreateDbContext();
 073        var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
 74
 075        if (filter.StartIndex.HasValue && filter.StartIndex > 0)
 76        {
 077            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 78        }
 79
 080        if (filter.Limit > 0)
 81        {
 082            dbQuery = dbQuery.OrderBy(e => e).Take(filter.Limit);
 83        }
 84
 085        return dbQuery.ToArray();
 086    }
 87
 88    /// <inheritdoc />
 89    public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
 90    {
 091        foreach (var person in people)
 92        {
 093            person.Name = person.Name.Trim();
 094            person.Role = person.Role?.Trim() ?? string.Empty;
 95        }
 96
 97        // multiple metadata providers can provide the _same_ person
 098        people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray();
 099        var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
 100
 0101        using var context = _dbProvider.CreateDbContext();
 0102        using var transaction = context.Database.BeginTransaction();
 0103        var existingPersons = context.Peoples.Select(e => new
 0104            {
 0105                item = e,
 0106                SelectionKey = e.Name + "-" + e.PersonType
 0107            })
 0108            .Where(p => personKeys.Contains(p.SelectionKey))
 0109            .Select(f => f.item)
 0110            .ToArray();
 111
 0112        var toAdd = people
 0113            .Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
 0114            .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
 0115            .Select(Map);
 0116        context.Peoples.AddRange(toAdd);
 0117        context.SaveChanges();
 118
 0119        var personsEntities = toAdd.Concat(existingPersons).ToArray();
 120
 0121        var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList();
 122
 0123        var listOrder = 0;
 124
 0125        foreach (var person in people)
 126        {
 0127            if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
 128            {
 129                continue;
 130            }
 131
 0132            var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString(
 0133            var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == pe
 0134            if (existingMap is null)
 135            {
 0136                context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
 0137                {
 0138                    Item = null!,
 0139                    ItemId = itemId,
 0140                    People = null!,
 0141                    PeopleId = entityPerson.Id,
 0142                    ListOrder = listOrder,
 0143                    SortOrder = person.SortOrder,
 0144                    Role = person.Role
 0145                });
 146            }
 147            else
 148            {
 149                // Update the order for existing mappings
 0150                existingMap.ListOrder = listOrder;
 0151                existingMap.SortOrder = person.SortOrder;
 152                // person mapping already exists so remove from list
 0153                existingMaps.Remove(existingMap);
 154            }
 155
 0156            listOrder++;
 157        }
 158
 0159        context.PeopleBaseItemMap.RemoveRange(existingMaps);
 160
 0161        context.SaveChanges();
 0162        transaction.Commit();
 0163    }
 164
 165    private PersonInfo Map(People people)
 166    {
 0167        var mapping = people.BaseItems?.FirstOrDefault();
 0168        var personInfo = new PersonInfo()
 0169        {
 0170            Id = people.Id,
 0171            Name = people.Name,
 0172            Role = mapping?.Role,
 0173            SortOrder = mapping?.SortOrder
 0174        };
 0175        if (Enum.TryParse<PersonKind>(people.PersonType, out var kind))
 176        {
 0177            personInfo.Type = kind;
 178        }
 179
 0180        return personInfo;
 181    }
 182
 183    private People Map(PersonInfo people)
 184    {
 0185        var personInfo = new People()
 0186        {
 0187            Name = people.Name,
 0188            PersonType = people.Type.ToString(),
 0189            Id = people.Id,
 0190        };
 191
 0192        return personInfo;
 193    }
 194
 195    private IQueryable<People> TranslateQuery(IQueryable<People> query, JellyfinDbContext context, InternalPeopleQuery f
 196    {
 0197        if (filter.User is not null && filter.IsFavorite.HasValue)
 198        {
 0199            var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0200            var oldQuery = query;
 201
 0202            query = context.UserData
 0203                .Where(u => u.Item!.Type == personType && u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.Us
 0204                .Join(oldQuery, e => e.Item!.Name, e => e.Name, (item, person) => person)
 0205                .Distinct()
 0206                .AsNoTracking();
 207        }
 208
 0209        if (!filter.ItemId.IsEmpty())
 210        {
 0211            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
 212        }
 213
 0214        if (filter.ParentId != null)
 215        {
 0216            query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentI
 217        }
 218
 0219        if (!filter.AppearsInItemId.IsEmpty())
 220        {
 0221            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
 222        }
 223
 0224        var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList();
 0225        if (queryPersonTypes.Count > 0)
 226        {
 0227            query = query.Where(e => queryPersonTypes.Contains(e.PersonType));
 228        }
 229
 0230        var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList();
 231
 0232        if (queryExcludePersonTypes.Count > 0)
 233        {
 0234            query = query.Where(e => !queryPersonTypes.Contains(e.PersonType));
 235        }
 236
 0237        if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty())
 238        {
 0239            query = query.Where(e => e.BaseItems!.Where(w => w.ItemId == filter.ItemId).OrderBy(w => w.ListOrder).First(
 240        }
 241
 0242        if (!string.IsNullOrWhiteSpace(filter.NameContains))
 243        {
 0244            var nameContainsUpper = filter.NameContains.ToUpper();
 0245            query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
 246        }
 247
 0248        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 249        {
 0250            query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant()));
 251        }
 252
 0253        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 254        {
 0255            query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0);
 256        }
 257
 0258        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 259        {
 0260            query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0);
 261        }
 262
 0263        return query;
 264    }
 265
 266    private bool IsAlphaNumeric(string str)
 267    {
 0268        if (string.IsNullOrWhiteSpace(str))
 269        {
 0270            return false;
 271        }
 272
 0273        for (int i = 0; i < str.Length; i++)
 274        {
 0275            if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
 276            {
 0277                return false;
 278            }
 279        }
 280
 0281        return true;
 282    }
 283
 284    private bool IsValidPersonType(string value)
 285    {
 0286        return IsAlphaNumeric(value);
 287    }
 288}