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();
var maxSortOrder = Math.Max(
context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId && e.People.PersonType == PersonKind.Actor.ToString()).Max(e => (int?)e.SortOrder) ?? 0,
people.Where(p => p.Type == PersonKind.Actor && p.SortOrder.HasValue).Max(p => (int?)p.SortOrder) ?? 0);
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.Type == PersonKind.Actor ? (person.SortOrder ?? ++maxSortOrder) : person.SortOrder;
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);
}
}