New Scanner + People Pages (#3286)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
1ed0eae22d
commit
ba20ad4ecc
142 changed files with 17529 additions and 3038 deletions
|
@ -95,59 +95,73 @@ public class AutoMapperProfiles : Profile
|
|||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.PagesRead));
|
||||
|
||||
CreateMap<SeriesMetadata, SeriesMetadataDto>()
|
||||
.ForMember(dest => dest.Writers,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Writer).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.CoverArtists,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.CoverArtist).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Characters,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Publishers,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Publisher).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Colorists,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Colorist).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Inkers,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Imprints,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Imprint).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Letterers,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Pencillers,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Penciller).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Translators,
|
||||
opt =>
|
||||
opt.MapFrom(src =>
|
||||
src.People.Where(p => p.Role == PersonRole.Translator).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Editors,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Teams,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Locations,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
|
||||
// Map Writers
|
||||
.ForMember(dest => dest.Writers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Writer)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map CoverArtists
|
||||
.ForMember(dest => dest.CoverArtists, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.CoverArtist)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Publishers
|
||||
.ForMember(dest => dest.Publishers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Publisher)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Characters
|
||||
.ForMember(dest => dest.Characters, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Character)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Pencillers
|
||||
.ForMember(dest => dest.Pencillers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Penciller)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Inkers
|
||||
.ForMember(dest => dest.Inkers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Inker)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Imprints
|
||||
.ForMember(dest => dest.Imprints, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Imprint)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Colorists
|
||||
.ForMember(dest => dest.Colorists, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Colorist)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Letterers
|
||||
.ForMember(dest => dest.Letterers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Letterer)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Editors
|
||||
.ForMember(dest => dest.Editors, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Editor)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Translators
|
||||
.ForMember(dest => dest.Translators, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Translator)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Teams
|
||||
.ForMember(dest => dest.Teams, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Team)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Locations
|
||||
.ForMember(dest => dest.Locations, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Location)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Genres,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
|
@ -157,89 +171,73 @@ public class AutoMapperProfiles : Profile
|
|||
opt.MapFrom(
|
||||
src => src.Tags.OrderBy(p => p.NormalizedTitle)));
|
||||
|
||||
CreateMap<Chapter, ChapterMetadataDto>()
|
||||
.ForMember(dest => dest.Writers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Writer).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.CoverArtists,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.CoverArtist).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Colorists,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Colorist).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Inkers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Imprints,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Imprint).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Letterers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Pencillers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Penciller).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Publishers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Publisher).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Translators,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Translator).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Characters,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Editors,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Teams,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Locations,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
|
||||
;
|
||||
|
||||
CreateMap<Chapter, ChapterDto>()
|
||||
.ForMember(dest => dest.Writers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Writer).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.CoverArtists,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.CoverArtist).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Colorists,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Colorist).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Inkers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Imprints,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Imprint).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Letterers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Pencillers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Penciller).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Publishers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Publisher).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Translators,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Translator).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Characters,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Editors,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Teams,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Locations,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
|
||||
;
|
||||
// Map Writers
|
||||
.ForMember(dest => dest.Writers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Writer)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map CoverArtists
|
||||
.ForMember(dest => dest.CoverArtists, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.CoverArtist)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Publishers
|
||||
.ForMember(dest => dest.Publishers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Publisher)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Characters
|
||||
.ForMember(dest => dest.Characters, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Character)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Pencillers
|
||||
.ForMember(dest => dest.Pencillers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Penciller)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Inkers
|
||||
.ForMember(dest => dest.Inkers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Inker)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Imprints
|
||||
.ForMember(dest => dest.Imprints, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Imprint)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Colorists
|
||||
.ForMember(dest => dest.Colorists, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Colorist)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Letterers
|
||||
.ForMember(dest => dest.Letterers, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Letterer)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Editors
|
||||
.ForMember(dest => dest.Editors, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Editor)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Translators
|
||||
.ForMember(dest => dest.Translators, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Translator)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Teams
|
||||
.ForMember(dest => dest.Teams, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Team)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)))
|
||||
// Map Locations
|
||||
.ForMember(dest => dest.Locations, opt => opt.MapFrom(src => src.People
|
||||
.Where(cp => cp.Role == PersonRole.Location)
|
||||
.Select(cp => cp.Person)
|
||||
.OrderBy(p => p.NormalizedName)));
|
||||
|
||||
|
||||
CreateMap<AppUser, UserDto>()
|
||||
.ForMember(dest => dest.AgeRestriction,
|
||||
|
@ -337,5 +335,11 @@ public class AutoMapperProfiles : Profile
|
|||
|
||||
|
||||
CreateMap<MangaFile, FileExtensionExportDto>();
|
||||
|
||||
CreateMap<Chapter, StandaloneChapterDto>()
|
||||
.ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId))
|
||||
.ForMember(dest => dest.VolumeTitle, opt => opt.MapFrom(src => src.Volume.Name))
|
||||
.ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId))
|
||||
.ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,4 +142,17 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
_chapter.CreatedUtc = created.ToUniversalTime();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChapterBuilder WithPerson(Person person, PersonRole role)
|
||||
{
|
||||
_chapter.People ??= new List<ChapterPeople>();
|
||||
_chapter.People.Add(new ChapterPeople()
|
||||
{
|
||||
Person = person,
|
||||
Role = role,
|
||||
Chapter = _chapter,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,15 +11,14 @@ public class PersonBuilder : IEntityBuilder<Person>
|
|||
private readonly Person _person;
|
||||
public Person Build() => _person;
|
||||
|
||||
public PersonBuilder(string name, PersonRole role)
|
||||
public PersonBuilder(string name)
|
||||
{
|
||||
_person = new Person()
|
||||
{
|
||||
Name = name.Trim(),
|
||||
NormalizedName = name.ToNormalized(),
|
||||
Role = role,
|
||||
ChapterMetadatas = new List<Chapter>(),
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
SeriesMetadataPeople = new List<SeriesMetadataPeople>(),
|
||||
ChapterPeople = new List<ChapterPeople>()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -34,10 +33,10 @@ public class PersonBuilder : IEntityBuilder<Person>
|
|||
return this;
|
||||
}
|
||||
|
||||
public PersonBuilder WithSeriesMetadata(SeriesMetadata metadata)
|
||||
public PersonBuilder WithSeriesMetadata(SeriesMetadataPeople seriesMetadataPeople)
|
||||
{
|
||||
_person.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
_person.SeriesMetadatas.Add(metadata);
|
||||
_person.SeriesMetadataPeople.Add(seriesMetadataPeople);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
|
|||
CollectionTags = new List<CollectionTag>(),
|
||||
Genres = new List<Genre>(),
|
||||
Tags = new List<Tag>(),
|
||||
People = new List<Person>()
|
||||
People = new List<SeriesMetadataPeople>()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -45,4 +45,17 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
|
|||
_seriesMetadata.AgeRating = rating;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SeriesMetadataBuilder WithPerson(Person person, PersonRole role)
|
||||
{
|
||||
_seriesMetadata.People ??= new List<SeriesMetadataPeople>();
|
||||
_seriesMetadata.People.Add(new SeriesMetadataPeople()
|
||||
{
|
||||
Role = role,
|
||||
Person = person,
|
||||
SeriesMetadata = _seriesMetadata,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ public interface ICacheHelper
|
|||
|
||||
bool CoverImageExists(string path);
|
||||
|
||||
bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile);
|
||||
bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile);
|
||||
bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile);
|
||||
bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile);
|
||||
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ public class CacheHelper : ICacheHelper
|
|||
/// <param name="forceUpdate"></param>
|
||||
/// <param name="firstFile"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile)
|
||||
public bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile)
|
||||
{
|
||||
return firstFile != null &&
|
||||
(!forceUpdate &&
|
||||
|
@ -71,7 +71,7 @@ public class CacheHelper : ICacheHelper
|
|||
/// <param name="forceUpdate">Should we ignore any logic and force this to return true</param>
|
||||
/// <param name="firstFile">The file in question</param>
|
||||
/// <returns></returns>
|
||||
public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile)
|
||||
public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile)
|
||||
{
|
||||
if (firstFile == null) return false;
|
||||
if (forceUpdate) return true;
|
||||
|
|
|
@ -1,153 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Helpers;
|
||||
#nullable enable
|
||||
|
||||
public static class GenreHelper
|
||||
{
|
||||
|
||||
public static void UpdateGenre(Dictionary<string, Genre> allGenres,
|
||||
IEnumerable<string> names, Action<Genre, bool> action)
|
||||
public static async Task UpdateChapterGenres(Chapter chapter, IEnumerable<string> genreNames, IUnitOfWork unitOfWork)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
var normalizedName = name.ToNormalized();
|
||||
if (string.IsNullOrEmpty(normalizedName)) continue;
|
||||
// Normalize genre names once and store them in a hash set for quick lookups
|
||||
var normalizedGenresToAdd = new HashSet<string>(genreNames.Select(g => g.ToNormalized()));
|
||||
|
||||
if (allGenres.TryGetValue(normalizedName, out var genre))
|
||||
// Remove genres that are no longer in the new list
|
||||
var genresToRemove = chapter.Genres
|
||||
.Where(g => !normalizedGenresToAdd.Contains(g.NormalizedTitle))
|
||||
.ToList();
|
||||
|
||||
if (genresToRemove.Count > 0)
|
||||
{
|
||||
foreach (var genreToRemove in genresToRemove)
|
||||
{
|
||||
action(genre, false);
|
||||
chapter.Genres.Remove(genreToRemove);
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
// Get all normalized titles to query the database for existing genres
|
||||
var existingGenreTitles = await unitOfWork.DataContext.Genre
|
||||
.Where(g => normalizedGenresToAdd.Contains(g.NormalizedTitle))
|
||||
.ToDictionaryAsync(g => g.NormalizedTitle);
|
||||
|
||||
// Find missing genres that are not in the database
|
||||
var missingGenres = normalizedGenresToAdd
|
||||
.Where(nt => !existingGenreTitles.ContainsKey(nt))
|
||||
.Select(title => new GenreBuilder(title).Build())
|
||||
.ToList();
|
||||
|
||||
// Add missing genres to the database
|
||||
if (missingGenres.Count > 0)
|
||||
{
|
||||
unitOfWork.DataContext.Genre.AddRange(missingGenres);
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
// Add newly inserted genres to existing genres dictionary for easier lookup
|
||||
foreach (var genre in missingGenres)
|
||||
{
|
||||
genre = new GenreBuilder(name).Build();
|
||||
allGenres.Add(normalizedName, genre);
|
||||
action(genre, true);
|
||||
existingGenreTitles[genre.NormalizedTitle] = genre;
|
||||
}
|
||||
}
|
||||
|
||||
// Add genres that are either existing or newly added to the chapter
|
||||
foreach (var normalizedTitle in normalizedGenresToAdd)
|
||||
{
|
||||
var genre = existingGenreTitles[normalizedTitle];
|
||||
|
||||
if (!chapter.Genres.Contains(genre))
|
||||
{
|
||||
chapter.Genres.Add(genre);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void KeepOnlySameGenreBetweenLists(ICollection<Genre> existingGenres, ICollection<Genre> removeAllExcept, Action<Genre>? action = null)
|
||||
{
|
||||
var existing = existingGenres.ToList();
|
||||
foreach (var genre in existing)
|
||||
{
|
||||
var existingPerson = removeAllExcept.FirstOrDefault(g => genre.NormalizedTitle != null && genre.NormalizedTitle.Equals(g.NormalizedTitle));
|
||||
if (existingPerson != null) continue;
|
||||
existingGenres.Remove(genre);
|
||||
action?.Invoke(genre);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the genre to the list if it's not already in there.
|
||||
/// </summary>
|
||||
/// <param name="metadataGenres"></param>
|
||||
/// <param name="genre"></param>
|
||||
public static void AddGenreIfNotExists(ICollection<Genre> metadataGenres, Genre genre)
|
||||
{
|
||||
var existingGenre = metadataGenres.FirstOrDefault(p =>
|
||||
p.NormalizedTitle.Equals(genre.Title?.ToNormalized()));
|
||||
if (existingGenre == null)
|
||||
{
|
||||
metadataGenres.Add(genre);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void UpdateGenreList(ICollection<GenreTagDto>? tags, Series series,
|
||||
IReadOnlyCollection<Genre> allTags, Action<Genre> handleAdd, Action onModified)
|
||||
public static void UpdateGenreList(ICollection<GenreTagDto>? existingGenres, Series series,
|
||||
IReadOnlyCollection<Genre> newGenres, Action<Genre> handleAdd, Action onModified)
|
||||
{
|
||||
// TODO: Write some unit tests
|
||||
if (tags == null) return;
|
||||
if (existingGenres == 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.Genres.ToList();
|
||||
|
||||
// Convert tags and existing genres to hash sets for quick lookups by normalized title
|
||||
var tagSet = new HashSet<string>(existingGenres.Select(t => t.Title.ToNormalized()));
|
||||
var genreSet = new HashSet<string>(series.Metadata.Genres.Select(g => g.NormalizedTitle));
|
||||
|
||||
// Remove tags that are no longer present in the input tags
|
||||
var existingTags = series.Metadata.Genres.ToList(); // Copy to avoid modifying collection while iterating
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (tags.SingleOrDefault(t => t.Title.ToNormalized().Equals(existing.NormalizedTitle)) == null)
|
||||
if (!tagSet.Contains(existing.NormalizedTitle)) // This correctly ensures removal of non-present tags
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.Genres.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
// Prepare a dictionary for quick lookup of genres from the `newGenres` collection by normalized title
|
||||
var allTagsDict = newGenres.ToDictionary(t => t.NormalizedTitle);
|
||||
|
||||
// Add new tags from the input list
|
||||
foreach (var tagDto in existingGenres)
|
||||
{
|
||||
var normalizedTitle = tagTitle.ToNormalized();
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
var normalizedTitle = tagDto.Title.ToNormalized();
|
||||
|
||||
if (!genreSet.Contains(normalizedTitle)) // This prevents re-adding existing genres
|
||||
{
|
||||
if (series.Metadata.Genres.All(t => !t.NormalizedTitle.Equals(normalizedTitle)))
|
||||
if (allTagsDict.TryGetValue(normalizedTitle, out var existingTag))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
handleAdd(existingTag); // Add existing tag from allTagsDict
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new GenreBuilder(tagTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateGenreList(ICollection<GenreTagDto>? tags, Chapter chapter,
|
||||
IReadOnlyCollection<Genre> allTags, Action<Genre> handleAdd, Action onModified)
|
||||
{
|
||||
// TODO: Write some unit tests
|
||||
if (tags == 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.Genres.ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (tags.SingleOrDefault(t => t.Title.ToNormalized().Equals(existing.NormalizedTitle)) == null)
|
||||
{
|
||||
// Remove tag
|
||||
chapter.Genres.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = tagTitle.ToNormalized();
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (chapter.Genres.All(t => !t.NormalizedTitle.Equals(normalizedTitle)))
|
||||
else
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
handleAdd(new GenreBuilder(tagDto.Title).Build()); // Add new genre if not found
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new GenreBuilder(tagTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Call onModified if any changes were made
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,198 +1,147 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Helpers;
|
||||
#nullable enable
|
||||
|
||||
public static class TagHelper
|
||||
{
|
||||
public static void UpdateTag(Dictionary<string, Tag> allTags, IEnumerable<string> names, Action<Tag, bool> action)
|
||||
|
||||
public static async Task UpdateChapterTags(Chapter chapter, IEnumerable<string> tagNames, IUnitOfWork unitOfWork)
|
||||
{
|
||||
foreach (var name in names)
|
||||
// Normalize tag names once and store them in a hash set for quick lookups
|
||||
var normalizedTagsToAdd = new HashSet<string>(tagNames.Select(t => t.ToNormalized()));
|
||||
var existingTagsSet = new HashSet<string>(chapter.Tags.Select(t => t.NormalizedTitle));
|
||||
|
||||
var isModified = false;
|
||||
|
||||
// Remove tags that are no longer present in the new list
|
||||
var tagsToRemove = chapter.Tags
|
||||
.Where(t => !normalizedTagsToAdd.Contains(t.NormalizedTitle))
|
||||
.ToList();
|
||||
|
||||
if (tagsToRemove.Any())
|
||||
{
|
||||
if (string.IsNullOrEmpty(name.Trim())) continue;
|
||||
|
||||
var normalizedName = name.ToNormalized();
|
||||
allTags.TryGetValue(normalizedName, out var tag);
|
||||
|
||||
var added = tag == null;
|
||||
if (tag == null)
|
||||
foreach (var tagToRemove in tagsToRemove)
|
||||
{
|
||||
tag = new TagBuilder(name).Build();
|
||||
allTags.Add(normalizedName, tag);
|
||||
chapter.Tags.Remove(tagToRemove);
|
||||
}
|
||||
|
||||
action(tag, added);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void KeepOnlySameTagBetweenLists(ICollection<Tag> existingTags, ICollection<Tag> removeAllExcept, Action<Tag>? action = null)
|
||||
{
|
||||
var existing = existingTags.ToList();
|
||||
foreach (var genre in existing)
|
||||
// Get all normalized titles for bulk lookup from the database
|
||||
var existingTagTitles = await unitOfWork.DataContext.Tag
|
||||
.Where(t => normalizedTagsToAdd.Contains(t.NormalizedTitle))
|
||||
.ToDictionaryAsync(t => t.NormalizedTitle);
|
||||
|
||||
// Find missing tags that are not already in the database
|
||||
var missingTags = normalizedTagsToAdd
|
||||
.Where(nt => !existingTagTitles.ContainsKey(nt))
|
||||
.Select(title => new TagBuilder(title).Build())
|
||||
.ToList();
|
||||
|
||||
// Add missing tags to the database if any
|
||||
if (missingTags.Any())
|
||||
{
|
||||
var existingPerson = removeAllExcept.FirstOrDefault(g => genre.NormalizedTitle.Equals(g.NormalizedTitle));
|
||||
if (existingPerson != null) continue;
|
||||
existingTags.Remove(genre);
|
||||
action?.Invoke(genre);
|
||||
unitOfWork.DataContext.Tag.AddRange(missingTags);
|
||||
await unitOfWork.CommitAsync(); // Commit once after adding missing tags to avoid multiple DB calls
|
||||
isModified = true;
|
||||
|
||||
// Update the dictionary with newly inserted tags for easier lookup
|
||||
foreach (var tag in missingTags)
|
||||
{
|
||||
existingTagTitles[tag.NormalizedTitle] = tag;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new or existing tags to the chapter
|
||||
foreach (var normalizedTitle in normalizedTagsToAdd)
|
||||
{
|
||||
var tag = existingTagTitles[normalizedTitle];
|
||||
|
||||
if (!existingTagsSet.Contains(normalizedTitle))
|
||||
{
|
||||
chapter.Tags.Add(tag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit changes if modifications were made to the chapter's tags
|
||||
if (isModified)
|
||||
{
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the tag to the list if it's not already in there. This will ignore the ExternalTag.
|
||||
/// Returns a list of strings separated by ',', distinct by normalized names, already trimmed and empty entries removed.
|
||||
/// </summary>
|
||||
/// <param name="metadataTags"></param>
|
||||
/// <param name="tag"></param>
|
||||
public static void AddTagIfNotExists(ICollection<Tag> metadataTags, Tag tag)
|
||||
{
|
||||
var existingGenre = metadataTags.FirstOrDefault(p =>
|
||||
p.NormalizedTitle == tag.Title.ToNormalized());
|
||||
if (existingGenre == null)
|
||||
{
|
||||
metadataTags.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddTagIfNotExists(BlockingCollection<Tag> metadataTags, Tag tag)
|
||||
{
|
||||
var existingGenre = metadataTags.FirstOrDefault(p =>
|
||||
p.NormalizedTitle == tag.Title.ToNormalized());
|
||||
if (existingGenre == null)
|
||||
{
|
||||
metadataTags.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="comicInfoTagSeparatedByComma"></param>
|
||||
/// <returns></returns>
|
||||
public static IList<string> GetTagValues(string comicInfoTagSeparatedByComma)
|
||||
{
|
||||
// TODO: Unit tests needed
|
||||
// TODO: Refactor this into an Extension
|
||||
if (string.IsNullOrEmpty(comicInfoTagSeparatedByComma))
|
||||
{
|
||||
return ImmutableList<string>.Empty;
|
||||
}
|
||||
|
||||
return comicInfoTagSeparatedByComma.Split(",")
|
||||
.Select(s => s.Trim())
|
||||
return comicInfoTagSeparatedByComma.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.DistinctBy(Parser.Normalize)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Remove tags on a list
|
||||
/// </summary>
|
||||
/// <remarks>Used to remove before we update/add new tags</remarks>
|
||||
/// <param name="existingTags">Existing tags on Entity</param>
|
||||
/// <param name="tags">Tags from metadata</param>
|
||||
/// <param name="action">Callback which will be executed for each tag removed</param>
|
||||
public static void RemoveTags(ICollection<Tag> existingTags, IEnumerable<string> tags, Action<Tag>? action = null)
|
||||
{
|
||||
var normalizedTags = tags.Select(Services.Tasks.Scanner.Parser.Parser.Normalize).ToList();
|
||||
foreach (var person in normalizedTags)
|
||||
{
|
||||
var existingTag = existingTags.FirstOrDefault(p => person.Equals(p.NormalizedTitle));
|
||||
if (existingTag == null) continue;
|
||||
|
||||
existingTags.Remove(existingTag);
|
||||
action?.Invoke(existingTag);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void UpdateTagList(ICollection<TagDto>? tags, Series series, IReadOnlyCollection<Tag> allTags, Action<Tag> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == 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.Tags.ToList();
|
||||
foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null))
|
||||
var existingTags = series.Metadata.Tags;
|
||||
|
||||
// Create a HashSet for quick lookup of tag IDs
|
||||
var tagIds = new HashSet<int>(tags.Select(t => t.Id));
|
||||
|
||||
// Remove tags that no longer exist in the provided tag list
|
||||
var tagsToRemove = existingTags.Where(existing => !tagIds.Contains(existing.Id)).ToList();
|
||||
if (tagsToRemove.Count > 0)
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.Tags.Remove(existing);
|
||||
foreach (var tagToRemove in tagsToRemove)
|
||||
{
|
||||
existingTags.Remove(tagToRemove);
|
||||
}
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = tagTitle.ToNormalized();
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle))
|
||||
{
|
||||
// Create a HashSet of normalized titles for quick lookups
|
||||
var normalizedTitlesToAdd = new HashSet<string>(tags.Select(t => t.Title.ToNormalized()));
|
||||
var existingNormalizedTitles = new HashSet<string>(existingTags.Select(t => t.NormalizedTitle));
|
||||
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new TagBuilder(tagTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
// Add missing tags based on normalized title comparison
|
||||
foreach (var normalizedTitle in normalizedTitlesToAdd)
|
||||
{
|
||||
if (existingNormalizedTitles.Contains(normalizedTitle)) continue;
|
||||
|
||||
var existingTag = allTags.FirstOrDefault(t => t.NormalizedTitle == normalizedTitle);
|
||||
handleAdd(existingTag ?? new TagBuilder(normalizedTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
// Call the modification handler if any changes were made
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateTagList(ICollection<TagDto>? tags, Chapter chapter, IReadOnlyCollection<Tag> allTags, Action<Tag> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == 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.Tags.ToList();
|
||||
foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null))
|
||||
{
|
||||
// Remove tag
|
||||
chapter.Tags.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = tagTitle.ToNormalized();
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (chapter.Tags.All(t => t.NormalizedTitle != normalizedTitle))
|
||||
{
|
||||
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new TagBuilder(tagTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue