< 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: 135
Coverable lines: 136
Total lines: 291
Line coverage: 0.7%
Branch coverage
0%
Covered branches: 0
Total branches: 66
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/13/2026 - 12:11: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: 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: 291 2/13/2026 - 12:11: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: 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: 291

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%
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    private PersonInfo Map(People people)
 169    {
 0170        var mapping = people.BaseItems?.FirstOrDefault();
 0171        var personInfo = new PersonInfo()
 0172        {
 0173            Id = people.Id,
 0174            Name = people.Name,
 0175            Role = mapping?.Role,
 0176            SortOrder = mapping?.SortOrder
 0177        };
 0178        if (Enum.TryParse<PersonKind>(people.PersonType, out var kind))
 179        {
 0180            personInfo.Type = kind;
 181        }
 182
 0183        return personInfo;
 184    }
 185
 186    private People Map(PersonInfo people)
 187    {
 0188        var personInfo = new People()
 0189        {
 0190            Name = people.Name,
 0191            PersonType = people.Type.ToString(),
 0192            Id = people.Id,
 0193        };
 194
 0195        return personInfo;
 196    }
 197
 198    private IQueryable<People> TranslateQuery(IQueryable<People> query, JellyfinDbContext context, InternalPeopleQuery f
 199    {
 0200        if (filter.User is not null && filter.IsFavorite.HasValue)
 201        {
 0202            var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0203            var oldQuery = query;
 204
 0205            query = context.UserData
 0206                .Where(u => u.Item!.Type == personType && u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.Us
 0207                .Join(oldQuery, e => e.Item!.Name, e => e.Name, (item, person) => person)
 0208                .Distinct()
 0209                .AsNoTracking();
 210        }
 211
 0212        if (!filter.ItemId.IsEmpty())
 213        {
 0214            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
 215        }
 216
 0217        if (filter.ParentId != null)
 218        {
 0219            query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentI
 220        }
 221
 0222        if (!filter.AppearsInItemId.IsEmpty())
 223        {
 0224            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
 225        }
 226
 0227        var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList();
 0228        if (queryPersonTypes.Count > 0)
 229        {
 0230            query = query.Where(e => queryPersonTypes.Contains(e.PersonType));
 231        }
 232
 0233        var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList();
 234
 0235        if (queryExcludePersonTypes.Count > 0)
 236        {
 0237            query = query.Where(e => !queryExcludePersonTypes.Contains(e.PersonType));
 238        }
 239
 0240        if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty())
 241        {
 0242            query = query.Where(e => e.BaseItems!.Where(w => w.ItemId == filter.ItemId).OrderBy(w => w.ListOrder).First(
 243        }
 244
 0245        if (!string.IsNullOrWhiteSpace(filter.NameContains))
 246        {
 0247            var nameContainsUpper = filter.NameContains.ToUpper();
 0248            query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
 249        }
 250
 0251        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 252        {
 0253            query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant()));
 254        }
 255
 0256        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 257        {
 0258            query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0);
 259        }
 260
 0261        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 262        {
 0263            query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0);
 264        }
 265
 0266        return query;
 267    }
 268
 269    private bool IsAlphaNumeric(string str)
 270    {
 0271        if (string.IsNullOrWhiteSpace(str))
 272        {
 0273            return false;
 274        }
 275
 0276        for (int i = 0; i < str.Length; i++)
 277        {
 0278            if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
 279            {
 0280                return false;
 281            }
 282        }
 283
 0284        return true;
 285    }
 286
 287    private bool IsValidPersonType(string value)
 288    {
 0289        return IsAlphaNumeric(value);
 290    }
 291}