mirror of
https://github.com/Kareadita/Kavita.git
synced 2026-05-28 10:32:34 -04:00
New Scanner + People Pages (#3286)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
+148
-164
@@ -1,210 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
|
||||
namespace API.Helpers;
|
||||
#nullable enable
|
||||
|
||||
// This isn't needed in the new person architecture
|
||||
public static class PersonHelper
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of all existing people, this will check the new names and roles and if it doesn't exist in allPeople, will create and
|
||||
/// add an entry. For each person in name, the callback will be executed.
|
||||
/// </summary>
|
||||
/// <remarks>This does not remove people if an empty list is passed into names</remarks>
|
||||
/// <remarks>This is used to add new people to a list without worrying about duplicating rows in the DB</remarks>
|
||||
/// <param name="allPeople"></param>
|
||||
/// <param name="names"></param>
|
||||
/// <param name="role"></param>
|
||||
/// <param name="action"></param>
|
||||
public static void UpdatePeople(ICollection<Person> allPeople, IEnumerable<string> names, PersonRole role, Action<Person> action)
|
||||
public static async Task UpdateSeriesMetadataPeopleAsync(SeriesMetadata metadata, ICollection<SeriesMetadataPeople> metadataPeople,
|
||||
IEnumerable<ChapterPeople> chapterPeople, PersonRole role, IUnitOfWork unitOfWork)
|
||||
{
|
||||
var allPeopleTypeRole = allPeople.Where(p => p.Role == role).ToList();
|
||||
var modification = false;
|
||||
|
||||
foreach (var name in names)
|
||||
// Get all normalized names of people with the specified role from chapterPeople
|
||||
var peopleToAdd = chapterPeople
|
||||
.Where(cp => cp.Role == role)
|
||||
.Select(cp => cp.Person.NormalizedName)
|
||||
.ToList();
|
||||
|
||||
// Prepare a HashSet for quick lookup of people to add
|
||||
var peopleToAddSet = new HashSet<string>(peopleToAdd);
|
||||
|
||||
// Get all existing people from metadataPeople with the specified role
|
||||
var existingMetadataPeople = metadataPeople
|
||||
.Where(mp => mp.Role == role)
|
||||
.ToList();
|
||||
|
||||
// Identify people to remove from metadataPeople
|
||||
var peopleToRemove = existingMetadataPeople
|
||||
.Where(person => !peopleToAddSet.Contains(person.Person.NormalizedName))
|
||||
.ToList();
|
||||
|
||||
// Remove identified people from metadataPeople
|
||||
foreach (var personToRemove in peopleToRemove)
|
||||
{
|
||||
var normalizedName = name.ToNormalized();
|
||||
// BUG: Doesn't this create a duplicate entry because allPeopleTypeRoles is a different instance?
|
||||
var person = allPeopleTypeRole.Find(p =>
|
||||
p.NormalizedName != null && p.NormalizedName.Equals(normalizedName));
|
||||
if (person == null)
|
||||
{
|
||||
person = new PersonBuilder(name, role).Build();
|
||||
allPeople.Add(person);
|
||||
}
|
||||
metadataPeople.Remove(personToRemove);
|
||||
modification = true;
|
||||
}
|
||||
|
||||
action(person);
|
||||
// Bulk fetch existing people from the repository
|
||||
var existingPeopleInDb = await unitOfWork.PersonRepository
|
||||
.GetPeopleByNames(peopleToAdd);
|
||||
|
||||
// Prepare a dictionary for quick lookup of existing people by normalized name
|
||||
var existingPeopleDict = new Dictionary<string, Person>();
|
||||
foreach (var person in existingPeopleInDb)
|
||||
{
|
||||
existingPeopleDict.TryAdd(person.NormalizedName, person);
|
||||
}
|
||||
|
||||
// Track the people to attach (newly created people)
|
||||
var peopleToAttach = new List<Person>();
|
||||
|
||||
// Identify new people (not already in metadataPeople) to add
|
||||
foreach (var personName in peopleToAdd)
|
||||
{
|
||||
// Check if the person already exists in metadataPeople with the specific role
|
||||
var personAlreadyInMetadata = metadataPeople
|
||||
.Any(mp => mp.Person.NormalizedName == personName && mp.Role == role);
|
||||
|
||||
if (!personAlreadyInMetadata)
|
||||
{
|
||||
// Check if the person exists in the database
|
||||
if (!existingPeopleDict.TryGetValue(personName, out var dbPerson))
|
||||
{
|
||||
// If not, create a new Person entity
|
||||
dbPerson = new PersonBuilder(personName).Build();
|
||||
peopleToAttach.Add(dbPerson); // Add new person to the list to be attached
|
||||
modification = true;
|
||||
}
|
||||
|
||||
// Add the person to the SeriesMetadataPeople collection
|
||||
metadataPeople.Add(new SeriesMetadataPeople
|
||||
{
|
||||
PersonId = dbPerson.Id, // EF Core will automatically update this after attach
|
||||
Person = dbPerson,
|
||||
SeriesMetadataId = metadata.Id,
|
||||
SeriesMetadata = metadata,
|
||||
Role = role
|
||||
});
|
||||
modification = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Attach all new people in one go (EF Core will assign IDs after commit)
|
||||
if (peopleToAttach.Count != 0)
|
||||
{
|
||||
await unitOfWork.DataContext.Person.AddRangeAsync(peopleToAttach);
|
||||
}
|
||||
|
||||
// Commit the changes if any modifications were made
|
||||
if (modification)
|
||||
{
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove people on a list for a given role
|
||||
/// </summary>
|
||||
/// <remarks>Used to remove before we update/add new people</remarks>
|
||||
/// <param name="existingPeople">Existing people on Entity</param>
|
||||
/// <param name="people">People from metadata</param>
|
||||
/// <param name="role">Role to filter on</param>
|
||||
/// <param name="action">Callback which will be executed for each person removed</param>
|
||||
public static void RemovePeople(ICollection<Person> existingPeople, IEnumerable<string> people, PersonRole role, Action<Person>? action = null)
|
||||
|
||||
public static async Task UpdateChapterPeopleAsync(Chapter chapter, IList<string> people, PersonRole role, IUnitOfWork unitOfWork)
|
||||
{
|
||||
var normalizedPeople = people.Select(Services.Tasks.Scanner.Parser.Parser.Normalize).ToList();
|
||||
if (normalizedPeople.Count == 0)
|
||||
{
|
||||
var peopleToRemove = existingPeople.Where(p => p.Role == role).ToList();
|
||||
foreach (var existingRoleToRemove in peopleToRemove)
|
||||
{
|
||||
existingPeople.Remove(existingRoleToRemove);
|
||||
action?.Invoke(existingRoleToRemove);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var modification = false;
|
||||
|
||||
foreach (var person in normalizedPeople)
|
||||
{
|
||||
var existingPerson = existingPeople.FirstOrDefault(p => p.Role == role && person.Equals(p.NormalizedName));
|
||||
if (existingPerson == null) continue;
|
||||
// Normalize the input names for comparison
|
||||
var normalizedPeople = people.Select(p => p.ToNormalized()).Distinct().ToList(); // Ensure distinct people
|
||||
|
||||
existingPeople.Remove(existingPerson);
|
||||
action?.Invoke(existingPerson);
|
||||
}
|
||||
// Get all existing ChapterPeople for the role
|
||||
var existingChapterPeople = chapter.People
|
||||
.Where(cp => cp.Role == role)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
// Prepare a hash set for quick lookup of existing people by name
|
||||
var existingPeopleNames = new HashSet<string>(existingChapterPeople.Select(cp => cp.Person.NormalizedName));
|
||||
|
||||
/// <summary>
|
||||
/// Removes all people that are not present in the removeAllExcept list.
|
||||
/// </summary>
|
||||
/// <param name="existingPeople"></param>
|
||||
/// <param name="removeAllExcept"></param>
|
||||
/// <param name="action">Callback for all entities that should be removed</param>
|
||||
public static void KeepOnlySamePeopleBetweenLists(IEnumerable<Person> existingPeople, ICollection<Person> removeAllExcept, Action<Person>? action = null)
|
||||
{
|
||||
// Bulk select all people from the repository whose names are in the provided list
|
||||
var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedPeople);
|
||||
|
||||
// Prepare a dictionary for quick lookup by normalized name
|
||||
var existingPeopleDict = new Dictionary<string, Person>();
|
||||
foreach (var person in existingPeople)
|
||||
{
|
||||
var existingPerson = removeAllExcept
|
||||
.FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName));
|
||||
if (existingPerson == null)
|
||||
{
|
||||
action?.Invoke(person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the person to the list if it's not already in there
|
||||
/// </summary>
|
||||
/// <param name="metadataPeople"></param>
|
||||
/// <param name="person"></param>
|
||||
public static void AddPersonIfNotExists(ICollection<Person> metadataPeople, Person person)
|
||||
{
|
||||
if (string.IsNullOrEmpty(person.Name)) return;
|
||||
var existingPerson = metadataPeople.FirstOrDefault(p =>
|
||||
p.NormalizedName == person.Name.ToNormalized() && p.Role == person.Role);
|
||||
|
||||
if (existingPerson == null)
|
||||
{
|
||||
metadataPeople.Add(person);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// For a given role and people dtos, update a series
|
||||
/// </summary>
|
||||
/// <param name="role"></param>
|
||||
/// <param name="people"></param>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="allPeople"></param>
|
||||
/// <param name="handleAdd">This will call with an existing or new tag, but the method does not update the series Metadata</param>
|
||||
/// <param name="onModified"></param>
|
||||
public static void UpdatePeopleList(PersonRole role, ICollection<PersonDto>? people, Series series, IReadOnlyCollection<Person> allPeople,
|
||||
Action<Person> handleAdd, Action onModified)
|
||||
{
|
||||
if (people == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (people.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.People.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
existingPeopleDict.TryAdd(person.NormalizedName, person);
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tag in people)
|
||||
// Identify people to remove (those present in ChapterPeople but not in the new list)
|
||||
foreach (var existingChapterPerson in existingChapterPeople
|
||||
.Where(existingChapterPerson => !normalizedPeople.Contains(existingChapterPerson.Person.NormalizedName)))
|
||||
{
|
||||
var existingTag = allPeople.FirstOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => t.Name != null && !t.Name.Equals(tag.Name)))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new PersonBuilder(tag.Name, role).Build());
|
||||
isModified = true;
|
||||
}
|
||||
chapter.People.Remove(existingChapterPerson);
|
||||
unitOfWork.PersonRepository.Remove(existingChapterPerson);
|
||||
modification = true;
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
// Identify new people to add
|
||||
var newPeopleNames = normalizedPeople
|
||||
.Where(p => !existingPeopleNames.Contains(p))
|
||||
.ToList();
|
||||
|
||||
public static void UpdatePeopleList(PersonRole role, ICollection<PersonDto>? people, Chapter chapter, IReadOnlyCollection<Person> allPeople,
|
||||
Action<Person> handleAdd, Action onModified)
|
||||
{
|
||||
if (people == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = chapter.People.Where(p => p.Role == role).ToList();
|
||||
foreach (var existing in existingTags)
|
||||
if (newPeopleNames.Count > 0)
|
||||
{
|
||||
if (people.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role
|
||||
// Bulk insert new people (if they don't already exist in the database)
|
||||
var newPeople = newPeopleNames
|
||||
.Where(name => !existingPeopleDict.ContainsKey(name)) // Avoid adding duplicates
|
||||
.Select(name => new PersonBuilder(name).Build())
|
||||
.ToList();
|
||||
|
||||
foreach (var newPerson in newPeople)
|
||||
{
|
||||
// Remove tag
|
||||
chapter.People.Remove(existing);
|
||||
isModified = true;
|
||||
unitOfWork.DataContext.Person.Attach(newPerson);
|
||||
existingPeopleDict[newPerson.NormalizedName] = newPerson;
|
||||
}
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
modification = true;
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tag in people)
|
||||
// Add all people (both existing and newly created) to the ChapterPeople
|
||||
foreach (var personName in normalizedPeople)
|
||||
{
|
||||
var existingTag = allPeople.FirstOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
|
||||
if (existingTag != null)
|
||||
var person = existingPeopleDict[personName];
|
||||
|
||||
// Check if the person with the specific role is already added to the chapter's People collection
|
||||
if (chapter.People.Any(cp => cp.PersonId == person.Id && cp.Role == role)) continue;
|
||||
|
||||
chapter.People.Add(new ChapterPeople
|
||||
{
|
||||
if (chapter.People.Where(t => t.Role == tag.Role).All(t => t.Name != null && !t.Name.Equals(tag.Name)))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new PersonBuilder(tag.Name, role).Build());
|
||||
isModified = true;
|
||||
}
|
||||
PersonId = person.Id,
|
||||
ChapterId = chapter.Id,
|
||||
Role = role
|
||||
});
|
||||
modification = true;
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
// Commit the changes to remove and add people
|
||||
if (modification)
|
||||
{
|
||||
onModified();
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +200,9 @@ public static class PersonHelper
|
||||
dto.Colorists.Count != 0 ||
|
||||
dto.Letterers.Count != 0 ||
|
||||
dto.Editors.Count != 0 ||
|
||||
dto.Translators.Count != 0;
|
||||
dto.Translators.Count != 0 ||
|
||||
dto.Teams.Count != 0 ||
|
||||
dto.Locations.Count != 0;
|
||||
}
|
||||
|
||||
public static bool HasAnyPeople(UpdateChapterDto? dto)
|
||||
@@ -235,6 +217,8 @@ public static class PersonHelper
|
||||
dto.Colorists.Count != 0 ||
|
||||
dto.Letterers.Count != 0 ||
|
||||
dto.Editors.Count != 0 ||
|
||||
dto.Translators.Count != 0;
|
||||
dto.Translators.Count != 0 ||
|
||||
dto.Teams.Count != 0 ||
|
||||
dto.Locations.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user