using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities.Libraries; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Item; #pragma warning disable RS0030 // Do not use banned APIs #pragma warning disable CA1304 // Specify CultureInfo #pragma warning disable CA1311 // Specify a culture or use an invariant version #pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons /// /// Manager for handling people. /// /// Efcore Factory. /// Items lookup service. /// /// Initializes a new instance of the class. /// public class PeopleRepository(IDbContextFactory dbProvider, IItemTypeLookup itemTypeLookup) : IPeopleRepository { private readonly IDbContextFactory _dbProvider = dbProvider; /// public IReadOnlyList GetPeople(InternalPeopleQuery filter) { using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); // Include PeopleBaseItemMap if (!filter.ItemId.IsEmpty()) { dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId)) .OrderBy(e => e.BaseItems!.First(e => e.ItemId == filter.ItemId).ListOrder) .ThenBy(e => e.PersonType) .ThenBy(e => e.Name); } else { dbQuery = dbQuery.OrderBy(e => e.Name); } if (filter.Limit > 0) { dbQuery = dbQuery.Take(filter.Limit); } return dbQuery.AsEnumerable().Select(Map).ToArray(); } /// public IReadOnlyList GetPeopleNames(InternalPeopleQuery filter) { using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct(); // dbQuery = dbQuery.OrderBy(e => e.ListOrder); if (filter.Limit > 0) { dbQuery = dbQuery.Take(filter.Limit); } return dbQuery.ToArray(); } /// public void UpdatePeople(Guid itemId, IReadOnlyList people) { // multiple metadata providers can provide the _same_ person people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray(); var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray(); using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); var existingPersons = context.Peoples.Select(e => new { item = e, SelectionKey = e.Name + "-" + e.PersonType }) .Where(p => personKeys.Contains(p.SelectionKey)) .Select(f => f.item) .ToArray(); var toAdd = people .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString())) .Select(Map); context.Peoples.AddRange(toAdd); context.SaveChanges(); var personsEntities = toAdd.Concat(existingPersons).ToArray(); var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList(); foreach (var person in people) { var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString()); var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.Role == person.Role); if (existingMap is null) { var sortOrder = (person.SortOrder ?? context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).Max(e => e.SortOrder) ?? 0) + 1; context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() { Item = null!, ItemId = itemId, People = null!, PeopleId = entityPerson.Id, ListOrder = sortOrder, SortOrder = sortOrder, Role = person.Role }); } else { // person mapping already exists so remove from list existingMaps.Remove(existingMap); } } context.PeopleBaseItemMap.RemoveRange(existingMaps); context.SaveChanges(); transaction.Commit(); } private PersonInfo Map(People people) { var mapping = people.BaseItems?.FirstOrDefault(); var personInfo = new PersonInfo() { Id = people.Id, Name = people.Name, Role = mapping?.Role, SortOrder = mapping?.SortOrder }; if (Enum.TryParse(people.PersonType, out var kind)) { personInfo.Type = kind; } return personInfo; } private People Map(PersonInfo people) { var personInfo = new People() { Name = people.Name, PersonType = people.Type.ToString(), Id = people.Id, }; return personInfo; } private IQueryable TranslateQuery(IQueryable query, JellyfinDbContext context, InternalPeopleQuery filter) { if (filter.User is not null && filter.IsFavorite.HasValue) { var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]; var oldQuery = query; query = context.UserData .Where(u => u.Item!.Type == personType && u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.User.Id)) .Join(oldQuery, e => e.Item!.Name, e => e.Name, (item, person) => person) .Distinct() .AsNoTracking(); } if (!filter.ItemId.IsEmpty()) { query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId))); } if (!filter.AppearsInItemId.IsEmpty()) { query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId))); } var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList(); if (queryPersonTypes.Count > 0) { query = query.Where(e => queryPersonTypes.Contains(e.PersonType)); } var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList(); if (queryExcludePersonTypes.Count > 0) { query = query.Where(e => !queryPersonTypes.Contains(e.PersonType)); } if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty()) { query = query.Where(e => e.BaseItems!.First(w => w.ItemId == filter.ItemId).ListOrder <= filter.MaxListOrder.Value); } if (!string.IsNullOrWhiteSpace(filter.NameContains)) { var nameContainsUpper = filter.NameContains.ToUpper(); query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper)); } return query; } private bool IsAlphaNumeric(string str) { if (string.IsNullOrWhiteSpace(str)) { return false; } for (int i = 0; i < str.Length; i++) { if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) { return false; } } return true; } private bool IsValidPersonType(string value) { return IsAlphaNumeric(value); } }