People Aliases and Merging (#3795)
Co-authored-by: Joseph Milazzo <josephmajora@gmail.com>
This commit is contained in:
parent
cd2a6af6f2
commit
7ce36bfc44
67 changed files with 5288 additions and 284 deletions
147
API/Services/PersonService.cs
Normal file
147
API/Services/PersonService.cs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities.Person;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public interface IPersonService
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds src as an alias to dst, this is a destructive operation
|
||||
/// </summary>
|
||||
/// <param name="src">Merged person</param>
|
||||
/// <param name="dst">Remaining person</param>
|
||||
/// <remarks>The entities passed as arguments **must** include all relations</remarks>
|
||||
/// <returns></returns>
|
||||
Task MergePeopleAsync(Person src, Person dst);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the alias to the person, requires that the aliases are not shared with anyone else
|
||||
/// </summary>
|
||||
/// <remarks>This method does NOT commit changes</remarks>
|
||||
/// <param name="person"></param>
|
||||
/// <param name="aliases"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases);
|
||||
}
|
||||
|
||||
public class PersonService(IUnitOfWork unitOfWork): IPersonService
|
||||
{
|
||||
|
||||
public async Task MergePeopleAsync(Person src, Person dst)
|
||||
{
|
||||
if (dst.Id == src.Id) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dst.Description) && !string.IsNullOrWhiteSpace(src.Description))
|
||||
{
|
||||
dst.Description = src.Description;
|
||||
}
|
||||
|
||||
if (dst.MalId == 0 && src.MalId != 0)
|
||||
{
|
||||
dst.MalId = src.MalId;
|
||||
}
|
||||
|
||||
if (dst.AniListId == 0 && src.AniListId != 0)
|
||||
{
|
||||
dst.AniListId = src.AniListId;
|
||||
}
|
||||
|
||||
if (dst.HardcoverId == null && src.HardcoverId != null)
|
||||
{
|
||||
dst.HardcoverId = src.HardcoverId;
|
||||
}
|
||||
|
||||
if (dst.Asin == null && src.Asin != null)
|
||||
{
|
||||
dst.Asin = src.Asin;
|
||||
}
|
||||
|
||||
if (dst.CoverImage == null && src.CoverImage != null)
|
||||
{
|
||||
dst.CoverImage = src.CoverImage;
|
||||
}
|
||||
|
||||
MergeChapterPeople(dst, src);
|
||||
MergeSeriesMetadataPeople(dst, src);
|
||||
|
||||
dst.Aliases.Add(new PersonAliasBuilder(src.Name).Build());
|
||||
|
||||
foreach (var alias in src.Aliases)
|
||||
{
|
||||
dst.Aliases.Add(alias);
|
||||
}
|
||||
|
||||
unitOfWork.PersonRepository.Remove(src);
|
||||
unitOfWork.PersonRepository.Update(dst);
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
private static void MergeChapterPeople(Person dst, Person src)
|
||||
{
|
||||
|
||||
foreach (var chapter in src.ChapterPeople)
|
||||
{
|
||||
var alreadyPresent = dst.ChapterPeople
|
||||
.Any(x => x.ChapterId == chapter.ChapterId && x.Role == chapter.Role);
|
||||
|
||||
if (alreadyPresent) continue;
|
||||
|
||||
dst.ChapterPeople.Add(new ChapterPeople
|
||||
{
|
||||
Role = chapter.Role,
|
||||
ChapterId = chapter.ChapterId,
|
||||
Person = dst,
|
||||
KavitaPlusConnection = chapter.KavitaPlusConnection,
|
||||
OrderWeight = chapter.OrderWeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void MergeSeriesMetadataPeople(Person dst, Person src)
|
||||
{
|
||||
foreach (var series in src.SeriesMetadataPeople)
|
||||
{
|
||||
var alreadyPresent = dst.SeriesMetadataPeople
|
||||
.Any(x => x.SeriesMetadataId == series.SeriesMetadataId && x.Role == series.Role);
|
||||
|
||||
if (alreadyPresent) continue;
|
||||
|
||||
dst.SeriesMetadataPeople.Add(new SeriesMetadataPeople
|
||||
{
|
||||
SeriesMetadataId = series.SeriesMetadataId,
|
||||
Role = series.Role,
|
||||
Person = dst,
|
||||
KavitaPlusConnection = series.KavitaPlusConnection,
|
||||
OrderWeight = series.OrderWeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases)
|
||||
{
|
||||
var normalizedAliases = aliases
|
||||
.Select(a => a.ToNormalized())
|
||||
.Where(a => !string.IsNullOrEmpty(a) && a != person.NormalizedName)
|
||||
.ToList();
|
||||
|
||||
if (normalizedAliases.Count == 0)
|
||||
{
|
||||
person.Aliases = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases);
|
||||
others = others.Where(p => p.Id != person.Id).ToList();
|
||||
|
||||
if (others.Count != 0) return false;
|
||||
|
||||
person.Aliases = aliases.Select(a => new PersonAliasBuilder(a).Build()).ToList();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using API.DTOs.Collection;
|
|||
using API.DTOs.KavitaPlus.ExternalMetadata;
|
||||
using API.DTOs.KavitaPlus.Metadata;
|
||||
using API.DTOs.Metadata.Matching;
|
||||
using API.DTOs.Person;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
|
@ -17,8 +18,10 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Entities.MetadataMatching;
|
||||
using API.Entities.Person;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
|
|
@ -614,12 +617,8 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
madeModification = await UpdateTags(series, settings, externalMetadata, processedTags) || madeModification;
|
||||
madeModification = UpdateAgeRating(series, settings, processedGenres.Concat(processedTags)) || madeModification;
|
||||
|
||||
var staff = (externalMetadata.Staff ?? []).Select(s =>
|
||||
{
|
||||
s.Name = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}";
|
||||
var staff = await SetNameAndAddAliases(settings, externalMetadata.Staff);
|
||||
|
||||
return s;
|
||||
}).ToList();
|
||||
madeModification = await UpdateWriters(series, settings, staff) || madeModification;
|
||||
madeModification = await UpdateArtists(series, settings, staff) || madeModification;
|
||||
madeModification = await UpdateCharacters(series, settings, externalMetadata.Characters) || madeModification;
|
||||
|
|
@ -632,6 +631,49 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
return madeModification;
|
||||
}
|
||||
|
||||
private async Task<List<SeriesStaffDto>> SetNameAndAddAliases(MetadataSettingsDto settings, IList<SeriesStaffDto>? staff)
|
||||
{
|
||||
if (staff == null || staff.Count == 0) return [];
|
||||
|
||||
var nameMappings = staff.Select(s => new
|
||||
{
|
||||
Staff = s,
|
||||
PreferredName = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}",
|
||||
AlternativeName = !settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}"
|
||||
}).ToList();
|
||||
|
||||
var preferredNames = nameMappings.Select(n => n.PreferredName.ToNormalized()).Distinct().ToList();
|
||||
var alternativeNames = nameMappings.Select(n => n.AlternativeName.ToNormalized()).Distinct().ToList();
|
||||
|
||||
var existingPeople = await _unitOfWork.PersonRepository.GetPeopleByNames(preferredNames.Union(alternativeNames).ToList());
|
||||
var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople);
|
||||
|
||||
var modified = false;
|
||||
foreach (var mapping in nameMappings)
|
||||
{
|
||||
mapping.Staff.Name = mapping.PreferredName;
|
||||
|
||||
if (existingPeopleDictionary.ContainsKey(mapping.PreferredName.ToNormalized()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (existingPeopleDictionary.TryGetValue(mapping.AlternativeName.ToNormalized(), out var person))
|
||||
{
|
||||
modified = true;
|
||||
person.Aliases.Add(new PersonAliasBuilder(mapping.PreferredName).Build());
|
||||
}
|
||||
}
|
||||
|
||||
if (modified)
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
return [.. staff];
|
||||
}
|
||||
|
||||
private static void GenerateGenreAndTagLists(ExternalSeriesDetailDto externalMetadata, MetadataSettingsDto settings,
|
||||
ref List<string> processedTags, ref List<string> processedGenres)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using API.Comparators;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Person;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
@ -361,8 +362,7 @@ public class SeriesService : ISeriesService
|
|||
var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames);
|
||||
|
||||
// Use a dictionary for quick lookups
|
||||
var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName)
|
||||
.ToDictionary(p => p.NormalizedName, p => p);
|
||||
var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople);
|
||||
|
||||
// List to track people that will be added to the metadata
|
||||
var peopleToAdd = new List<Person>();
|
||||
|
|
|
|||
|
|
@ -579,7 +579,7 @@ public class CoverDbService : ICoverDbService
|
|||
else
|
||||
{
|
||||
_directoryService.DeleteFiles([tempFullPath]);
|
||||
series.CoverImage = Path.GetFileName(existingPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue