Basic Metadata Polish (#3548)

This commit is contained in:
Joe Milazzo 2025-02-14 15:23:52 -06:00 committed by GitHub
parent c0b59d87a4
commit 4c44dbf3e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 3596 additions and 467 deletions

View file

@ -64,7 +64,14 @@ public class LicenseController(
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
public async Task<ActionResult<LicenseInfoDto?>> GetLicenseInfo(bool forceCheck = false)
{
return Ok(await licenseService.GetLicenseInfo(forceCheck));
try
{
return Ok(await licenseService.GetLicenseInfo(forceCheck));
}
catch (Exception)
{
return Ok(null);
}
}
[Authorize("RequireAdminRole")]

View file

@ -15,7 +15,7 @@ public class ExternalSeriesDetailDto
public string Name { get; set; }
public int? AniListId { get; set; }
public long? MALId { get; set; }
public IList<string> Synonyms { get; set; }
public IList<string> Synonyms { get; set; } = [];
public PlusMediaFormat PlusMediaFormat { get; set; }
public string? SiteUrl { get; set; }
public string? CoverUrl { get; set; }
@ -30,8 +30,8 @@ public class ExternalSeriesDetailDto
public int AverageScore { get; set; }
public int Chapters { get; set; }
public int Volumes { get; set; }
public IList<SeriesRelationship>? Relations { get; set; }
public IList<SeriesCharacter>? Characters { get; set; }
public IList<SeriesRelationship>? Relations { get; set; } = [];
public IList<SeriesCharacter>? Characters { get; set; } = [];
}

View file

@ -230,9 +230,10 @@ public class PersonRepository : IPersonRepository
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
{
List<PersonRole> notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator];
return await _context.Person
.Where(p => p.Id == personId)
.SelectMany(p => p.SeriesMetadataPeople)
.SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role)))
.Select(smp => smp.SeriesMetadata)
.Select(sm => sm.Series)
.Distinct()

View file

@ -75,6 +75,7 @@ public interface ISeriesRepository
{
void Add(Series series);
void Attach(Series series);
void Attach(SeriesRelation relation);
void Update(Series series);
void Remove(Series series);
void Remove(IEnumerable<Series> series);
@ -146,6 +147,9 @@ public interface ISeriesRepository
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
int userId, SeriesIncludes includes = SeriesIncludes.None);
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
Task<Series?> GetSeriesByAnyName(IList<string> names, IList<MangaFormat> formats,
int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None);
Task<Series?> GetSeriesByAnyName(string seriesName, string localizedName, IList<MangaFormat> formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None);
public Task<IList<Series>> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId,
MangaFormat format);
@ -195,6 +199,11 @@ public class SeriesRepository : ISeriesRepository
_context.Series.Attach(series);
}
public void Attach(SeriesRelation relation)
{
_context.SeriesRelation.Attach(relation);
}
public void Attach(ExternalSeriesMetadata metadata)
{
_context.ExternalSeriesMetadata.Attach(metadata);
@ -1757,6 +1766,41 @@ public class SeriesRepository : ISeriesRepository
.FirstOrDefaultAsync();
}
public async Task<Series?> GetSeriesByAnyName(IList<string> names, IList<MangaFormat> formats,
int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None)
{
var libraryIds = GetLibraryIdsForUser(userId);
names = names.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList();
var normalizedNames = names.Select(s => s.ToNormalized()).ToList();
var query = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Where(s => formats.Contains(s.Format));
if (aniListId.HasValue && aniListId.Value > 0)
{
// If AniList ID is provided, override name checks
query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value ||
normalizedNames.Contains(s.NormalizedName)
|| normalizedNames.Contains(s.NormalizedLocalizedName)
|| names.Contains(s.OriginalName));
}
else
{
// Otherwise, use name checks
query = query.Where(s =>
normalizedNames.Contains(s.NormalizedName)
|| normalizedNames.Contains(s.NormalizedLocalizedName)
|| names.Contains(s.OriginalName));
}
return await query
.Includes(includes)
.FirstOrDefaultAsync();
}
public async Task<IList<Series>> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId,
MangaFormat format)
{

View file

@ -26,9 +26,10 @@ public static class PlusMediaFormatExtensions
{
return plusMediaFormat switch
{
PlusMediaFormat.Manga => new[] { LibraryType.Manga, LibraryType.Image },
PlusMediaFormat.Comic => new[] { LibraryType.Comic, LibraryType.ComicVine },
PlusMediaFormat.LightNovel => new[] { LibraryType.LightNovel, LibraryType.Book, LibraryType.Manga },
PlusMediaFormat.Manga => [LibraryType.Manga, LibraryType.Image],
PlusMediaFormat.Comic => [LibraryType.Comic, LibraryType.ComicVine],
PlusMediaFormat.LightNovel => [LibraryType.LightNovel, LibraryType.Book, LibraryType.Manga],
PlusMediaFormat.Book => [LibraryType.LightNovel, LibraryType.Book],
_ => throw new ArgumentOutOfRangeException(nameof(plusMediaFormat), plusMediaFormat, null)
};
}

View file

@ -61,4 +61,10 @@ public class AppUserBuilder : IEntityBuilder<AppUser>
return this;
}
public AppUserBuilder WithRole(string role)
{
_appUser.UserRoles ??= new List<AppUserRole>();
_appUser.UserRoles.Add(new AppUserRole() {Role = new AppRole() {Name = role}});
return this;
}
}

View file

@ -21,11 +21,13 @@ public class SeriesBuilder : IEntityBuilder<Series>
_series = new Series()
{
Name = name,
LocalizedName = name.ToNormalized(),
NormalizedLocalizedName = name.ToNormalized(),
OriginalName = name,
SortName = name,
NormalizedName = name.ToNormalized(),
NormalizedLocalizedName = name.ToNormalized(),
Metadata = new SeriesMetadataBuilder()
.WithPublicationStatus(PublicationStatus.OnGoing)
.Build(),
@ -39,14 +41,25 @@ public class SeriesBuilder : IEntityBuilder<Series>
/// </summary>
/// <param name="localizedName"></param>
/// <returns></returns>
public SeriesBuilder WithLocalizedName(string localizedName)
public SeriesBuilder WithLocalizedName(string localizedName, bool lockStatus = false)
{
// Why is this here?
if (string.IsNullOrEmpty(localizedName))
{
localizedName = _series.Name;
}
_series.LocalizedName = localizedName;
_series.NormalizedLocalizedName = localizedName.ToNormalized();
_series.LocalizedNameLocked = lockStatus;
return this;
}
public SeriesBuilder WithLocalizedNameAllowEmpty(string localizedName, bool lockStatus = false)
{
_series.LocalizedName = localizedName;
_series.NormalizedLocalizedName = localizedName.ToNormalized();
_series.LocalizedNameLocked = lockStatus;
return this;
}
@ -106,4 +119,15 @@ public class SeriesBuilder : IEntityBuilder<Series>
}
public SeriesBuilder WithRelationship(int targetSeriesId, RelationKind kind)
{
_series.Relations ??= [];
_series.Relations.Add(new SeriesRelation()
{
RelationKind = kind,
TargetSeriesId = targetSeriesId
});
return this;
}
}

View file

@ -39,15 +39,17 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
return this;
}
public SeriesMetadataBuilder WithPublicationStatus(PublicationStatus status)
public SeriesMetadataBuilder WithPublicationStatus(PublicationStatus status, bool lockState = false)
{
_seriesMetadata.PublicationStatus = status;
_seriesMetadata.PublicationStatusLocked = lockState;
return this;
}
public SeriesMetadataBuilder WithAgeRating(AgeRating rating)
public SeriesMetadataBuilder WithAgeRating(AgeRating rating, bool lockState = false)
{
_seriesMetadata.AgeRating = rating;
_seriesMetadata.AgeRatingLocked = lockState;
return this;
}
@ -60,7 +62,6 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
Person = person,
SeriesMetadata = _seriesMetadata,
});
return this;
}
@ -70,15 +71,40 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
return this;
}
public SeriesMetadataBuilder WithReleaseYear(int year)
public SeriesMetadataBuilder WithReleaseYear(int year, bool lockStatus = false)
{
_seriesMetadata.ReleaseYear = year;
_seriesMetadata.ReleaseYearLocked = lockStatus;
return this;
}
public SeriesMetadataBuilder WithSummary(string summary)
public SeriesMetadataBuilder WithSummary(string summary, bool lockStatus = false)
{
_seriesMetadata.Summary = summary;
_seriesMetadata.SummaryLocked = lockStatus;
return this;
}
public SeriesMetadataBuilder WithGenre(Genre genre, bool lockStatus = false)
{
_seriesMetadata.Genres ??= [];
_seriesMetadata.Genres.Add(genre);
_seriesMetadata.GenresLocked = lockStatus;
return this;
}
public SeriesMetadataBuilder WithGenres(List<Genre> genres, bool lockStatus = false)
{
_seriesMetadata.Genres = genres;
_seriesMetadata.GenresLocked = lockStatus;
return this;
}
public SeriesMetadataBuilder WithTag(Tag tag, bool lockStatus = false)
{
_seriesMetadata.Tags ??= [];
_seriesMetadata.Tags.Add(tag);
_seriesMetadata.TagsLocked = lockStatus;
return this;
}
}

View file

@ -0,0 +1,42 @@
using System.Text.RegularExpressions;
namespace API.Helpers;
#nullable enable
public static class StringHelper
{
/// <summary>
/// Used to squash duplicate break and new lines with a single new line.
/// </summary>
/// <example>Test br br Test -> Test br Test</example>
/// <param name="summary"></param>
/// <returns></returns>
public static string? SquashBreaklines(string? summary)
{
if (string.IsNullOrWhiteSpace(summary))
{
return null;
}
// First standardize all br tags to <br /> format
summary = Regex.Replace(summary, @"<br\s*/?>", "<br />", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Replace multiple consecutive br tags with a single br tag
summary = Regex.Replace(summary, @"(?:<br />\s*)+", "<br /> ", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Normalize remaining whitespace (replace multiple spaces with a single space)
summary = Regex.Replace(summary, @"\s+", " ").Trim();
return summary.Trim();
}
/// <summary>
/// Removes the (Source: MangaDex) type of tags at the end of descriptions from AL
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
public static string? RemoveSourceInDescription(string? description)
{
return description?.Trim();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,15 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Constants;
using API.Controllers;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.CollectionTags;
using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Interfaces;
using API.Entities.Metadata;
using API.Extensions;
using API.Helpers;
@ -22,7 +17,6 @@ using API.Helpers.Builders;
using API.Services.Plus;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR;
using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Microsoft.Extensions.Logging;
@ -56,7 +50,6 @@ public class SeriesService : ISeriesService
private readonly ILogger<SeriesService> _logger;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
private readonly IImageService _imageService;
private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto
{
@ -66,7 +59,7 @@ public class SeriesService : ISeriesService
};
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService, IImageService imageService)
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
@ -74,7 +67,6 @@ public class SeriesService : ISeriesService
_logger = logger;
_scrobblingService = scrobblingService;
_localizationService = localizationService;
_imageService = imageService;
}
/// <summary>