< 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: 156
Coverable lines: 157
Total lines: 327
Line coverage: 0.6%
Branch coverage
0%
Covered branches: 0
Total branches: 72
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/5/2026 - 12:13:57 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: 2885/7/2026 - 12:15:44 AM Line coverage: 0.7% (1/138) Branch coverage: 0% (0/70) Total lines: 2965/11/2026 - 12:15:59 AM Line coverage: 0.7% (1/138) Branch coverage: 0% (0/70) Total lines: 2975/14/2026 - 12:15:54 AM Line coverage: 0.7% (1/136) Branch coverage: 0% (0/66) Total lines: 2915/28/2026 - 12:15:50 AM Line coverage: 0.6% (1/149) Branch coverage: 0% (0/70) Total lines: 3166/1/2026 - 12:16:05 AM Line coverage: 0.6% (1/157) Branch coverage: 0% (0/72) Total lines: 327 3/5/2026 - 12:13:57 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: 2885/7/2026 - 12:15:44 AM Line coverage: 0.7% (1/138) Branch coverage: 0% (0/70) Total lines: 2965/11/2026 - 12:15:59 AM Line coverage: 0.7% (1/138) Branch coverage: 0% (0/70) Total lines: 2975/14/2026 - 12:15:54 AM Line coverage: 0.7% (1/136) Branch coverage: 0% (0/66) Total lines: 2915/28/2026 - 12:15:50 AM Line coverage: 0.6% (1/149) Branch coverage: 0% (0/70) Total lines: 3166/1/2026 - 12:16:05 AM Line coverage: 0.6% (1/157) Branch coverage: 0% (0/72) Total lines: 327

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%110100%
GetPeopleNamesByItems(...)0%4260%
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        {
 47            // The Peoples table has one row per (Name, PersonType), so the same person can
 48            // appear multiple times (e.g. as Actor and GuestStar). Collapse to one row per
 49            // name so /Persons doesn't return the same BaseItem id repeatedly. Lowercase the
 50            // grouping key so case-only duplicates collapse together.
 051            var representativeIds = dbQuery
 052                .GroupBy(e => e.Name.ToLower())
 053                .Select(g => g.Min(e => e.Id));
 054            dbQuery = context.Peoples.AsNoTracking()
 055                .Where(p => representativeIds.Contains(p.Id))
 056                .OrderBy(e => e.Name);
 57        }
 58
 059        var count = dbQuery.Count();
 060        if (filter.StartIndex.HasValue && filter.StartIndex > 0)
 61        {
 062            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 63        }
 64
 065        if (filter.Limit > 0)
 66        {
 067            dbQuery = dbQuery.Take(filter.Limit);
 68        }
 69
 070        return new QueryResult<PersonInfo>
 071        {
 072            StartIndex = filter.StartIndex ?? 0,
 073            TotalRecordCount = count,
 074            Items = dbQuery.AsEnumerable().Select(Map).ToArray(),
 075        };
 076    }
 77
 78    /// <inheritdoc/>
 79    public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter)
 80    {
 081        using var context = _dbProvider.CreateDbContext();
 082        var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
 83
 084        if (filter.StartIndex.HasValue && filter.StartIndex > 0)
 85        {
 086            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 87        }
 88
 089        if (filter.Limit > 0)
 90        {
 091            dbQuery = dbQuery.OrderBy(e => e).Take(filter.Limit);
 92        }
 93
 094        return dbQuery.ToArray();
 095    }
 96
 97    /// <inheritdoc />
 98    public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
 99    {
 0100        foreach (var person in people)
 101        {
 0102            person.Name = person.Name.Trim();
 0103            person.Role = person.Role?.Trim() ?? string.Empty;
 104        }
 105
 106        // multiple metadata providers can provide the _same_ person; dedupe case-insensitively.
 0107        people = people.DistinctBy(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray();
 0108        var personKeys = people.Select(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray();
 109
 0110        using var context = _dbProvider.CreateDbContext();
 0111        using var transaction = context.Database.BeginTransaction();
 0112        var existingPersons = context.Peoples.Select(e => new
 0113        {
 0114            item = e,
 0115            SelectionKey = e.Name.ToLower() + "-" + e.PersonType
 0116        })
 0117            .Where(p => personKeys.Contains(p.SelectionKey))
 0118            .Select(f => f.item)
 0119            .ToArray();
 120
 0121        var toAdd = people
 0122            .Where(e => !existingPersons.Any(f => string.Equals(f.Name, e.Name, StringComparison.OrdinalIgnoreCase) && f
 0123            .Select(Map);
 0124        context.Peoples.AddRange(toAdd);
 0125        context.SaveChanges();
 126
 0127        var personsEntities = toAdd.Concat(existingPersons).ToArray();
 128
 0129        var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList();
 130
 0131        var listOrder = 0;
 132
 0133        foreach (var person in people)
 134        {
 0135            var entityPerson = personsEntities.First(e => string.Equals(e.Name, person.Name, StringComparison.OrdinalIgn
 0136            var existingMap = existingMaps.FirstOrDefault(e => string.Equals(e.People.Name, person.Name, StringCompariso
 0137            if (existingMap is null)
 138            {
 0139                context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
 0140                {
 0141                    Item = null!,
 0142                    ItemId = itemId,
 0143                    People = null!,
 0144                    PeopleId = entityPerson.Id,
 0145                    ListOrder = listOrder,
 0146                    SortOrder = person.SortOrder,
 0147                    Role = person.Role
 0148                });
 149            }
 150            else
 151            {
 152                // Update the order for existing mappings
 0153                existingMap.ListOrder = listOrder;
 0154                existingMap.SortOrder = person.SortOrder;
 155                // person mapping already exists so remove from list
 0156                existingMaps.Remove(existingMap);
 157            }
 158
 0159            listOrder++;
 160        }
 161
 0162        context.PeopleBaseItemMap.RemoveRange(existingMaps);
 163
 0164        context.SaveChanges();
 0165        transaction.Commit();
 0166    }
 167
 168    /// <inheritdoc/>
 169    public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnly
 170    {
 0171        using var context = _dbProvider.CreateDbContext();
 0172        var query = context.PeopleBaseItemMap
 0173            .AsNoTracking()
 0174            .Where(m => itemIds.Contains(m.ItemId));
 175
 0176        if (personTypes.Count > 0)
 177        {
 0178            query = query.Where(m => personTypes.Contains(m.People.PersonType));
 179        }
 180
 0181        var rows = query
 0182            .OrderBy(m => m.ListOrder)
 0183            .Select(m => new { m.ItemId, m.People.Name })
 0184            .ToList();
 185
 0186        var result = new Dictionary<Guid, IReadOnlyList<string>>();
 0187        foreach (var group in rows.GroupBy(r => r.ItemId))
 188        {
 0189            var names = group
 0190                .Select(r => r.Name)
 0191                .Where(name => !string.IsNullOrEmpty(name))
 0192                .Distinct()
 0193                .ToArray();
 194
 0195            if (names.Length > 0)
 196            {
 0197                result[group.Key] = names;
 198            }
 199        }
 200
 0201        return result;
 0202    }
 203
 204    private PersonInfo Map(People people)
 205    {
 0206        var mapping = people.BaseItems?.FirstOrDefault();
 0207        var personInfo = new PersonInfo()
 0208        {
 0209            Id = people.Id,
 0210            Name = people.Name,
 0211            Role = mapping?.Role,
 0212            SortOrder = mapping?.SortOrder
 0213        };
 0214        if (Enum.TryParse<PersonKind>(people.PersonType, out var kind))
 215        {
 0216            personInfo.Type = kind;
 217        }
 218
 0219        return personInfo;
 220    }
 221
 222    private People Map(PersonInfo people)
 223    {
 0224        var personInfo = new People()
 0225        {
 0226            Name = people.Name,
 0227            PersonType = people.Type.ToString(),
 0228            Id = people.Id,
 0229        };
 230
 0231        return personInfo;
 232    }
 233
 234    private IQueryable<People> TranslateQuery(IQueryable<People> query, JellyfinDbContext context, InternalPeopleQuery f
 235    {
 0236        if (filter.User is not null && filter.IsFavorite.HasValue)
 237        {
 0238            var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0239            var oldQuery = query;
 240
 0241            query = context.UserData
 0242                .Where(u => u.Item!.Type == personType && u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.Us
 0243                .Join(oldQuery, e => e.Item!.Name, e => e.Name, (item, person) => person)
 0244                .Distinct()
 0245                .AsNoTracking();
 246        }
 247
 0248        if (!filter.ItemId.IsEmpty())
 249        {
 0250            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
 251        }
 252
 0253        if (filter.ParentId != null)
 254        {
 0255            query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentI
 256        }
 257
 0258        if (!filter.AppearsInItemId.IsEmpty())
 259        {
 0260            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
 261        }
 262
 0263        var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList();
 0264        if (queryPersonTypes.Count > 0)
 265        {
 0266            query = query.Where(e => queryPersonTypes.Contains(e.PersonType));
 267        }
 268
 0269        var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList();
 270
 0271        if (queryExcludePersonTypes.Count > 0)
 272        {
 0273            query = query.Where(e => !queryExcludePersonTypes.Contains(e.PersonType));
 274        }
 275
 0276        if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty())
 277        {
 0278            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId == filter.ItemId && w.ListOrder <= filter.MaxListOrd
 279        }
 280
 0281        if (!string.IsNullOrWhiteSpace(filter.NameContains))
 282        {
 0283            var nameContainsUpper = filter.NameContains.ToUpper();
 0284            query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
 285        }
 286
 0287        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 288        {
 0289            query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant()));
 290        }
 291
 0292        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 293        {
 0294            query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0);
 295        }
 296
 0297        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 298        {
 0299            query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0);
 300        }
 301
 0302        return query;
 303    }
 304
 305    private bool IsAlphaNumeric(string str)
 306    {
 0307        if (string.IsNullOrWhiteSpace(str))
 308        {
 0309            return false;
 310        }
 311
 0312        for (int i = 0; i < str.Length; i++)
 313        {
 0314            if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
 315            {
 0316                return false;
 317            }
 318        }
 319
 0320        return true;
 321    }
 322
 323    private bool IsValidPersonType(string value)
 324    {
 0325        return IsAlphaNumeric(value);
 326    }
 327}