A few more bug fixes (#3876)

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Fesaa 2025-06-28 18:45:02 +02:00 committed by GitHub
parent 4b9bbc5d78
commit d909e03baf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 3940 additions and 45 deletions

View File

@ -9,6 +9,7 @@ using API.DTOs;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.MetadataMatching;
using API.Entities.Person; using API.Entities.Person;
using API.Extensions; using API.Extensions;
using API.Helpers; using API.Helpers;
@ -208,6 +209,7 @@ public class ChapterController : BaseApiController
if (chapter.AgeRating != dto.AgeRating) if (chapter.AgeRating != dto.AgeRating)
{ {
chapter.AgeRating = dto.AgeRating; chapter.AgeRating = dto.AgeRating;
chapter.KPlusOverrides.Remove(MetadataSettingField.AgeRating);
} }
dto.Summary ??= string.Empty; dto.Summary ??= string.Empty;
@ -215,6 +217,7 @@ public class ChapterController : BaseApiController
if (chapter.Summary != dto.Summary.Trim()) if (chapter.Summary != dto.Summary.Trim())
{ {
chapter.Summary = dto.Summary.Trim(); chapter.Summary = dto.Summary.Trim();
chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterSummary);
} }
if (chapter.Language != dto.Language) if (chapter.Language != dto.Language)
@ -230,11 +233,13 @@ public class ChapterController : BaseApiController
if (chapter.TitleName != dto.TitleName) if (chapter.TitleName != dto.TitleName)
{ {
chapter.TitleName = dto.TitleName; chapter.TitleName = dto.TitleName;
chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterTitle);
} }
if (chapter.ReleaseDate != dto.ReleaseDate) if (chapter.ReleaseDate != dto.ReleaseDate)
{ {
chapter.ReleaseDate = dto.ReleaseDate; chapter.ReleaseDate = dto.ReleaseDate;
chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterReleaseDate);
} }
if (!string.IsNullOrEmpty(dto.ISBN) && ArticleNumberHelper.IsValidIsbn10(dto.ISBN) || if (!string.IsNullOrEmpty(dto.ISBN) && ArticleNumberHelper.IsValidIsbn10(dto.ISBN) ||
@ -333,6 +338,8 @@ public class ChapterController : BaseApiController
_unitOfWork _unitOfWork
); );
// TODO: Only remove field if changes were made
chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher);
// Update publishers // Update publishers
await PersonHelper.UpdateChapterPeopleAsync( await PersonHelper.UpdateChapterPeopleAsync(
chapter, chapter,

View File

@ -6,6 +6,7 @@ using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs.Koreader; using API.DTOs.Koreader;
using API.Entities; using API.Entities;
using API.Extensions;
using API.Services; using API.Services;
using Kavita.Common; using Kavita.Common;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -94,7 +95,7 @@ public class KoreaderController : BaseApiController
{ {
var userId = await GetUserId(apiKey); var userId = await GetUserId(apiKey);
var response = await _koreaderService.GetProgress(ebookHash, userId); var response = await _koreaderService.GetProgress(ebookHash, userId);
_logger.LogDebug("Koreader response progress: {Progress}", response.Progress); _logger.LogDebug("Koreader response progress for User ({UserId}): {Progress}", userId, response.Progress.Sanitize());
return Ok(response); return Ok(response);
} }

View File

@ -14,6 +14,7 @@ using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.MetadataMatching;
using API.Extensions; using API.Extensions;
using API.Helpers; using API.Helpers;
using API.Services; using API.Services;
@ -224,6 +225,7 @@ public class SeriesController : BaseApiController
needsRefreshMetadata = true; needsRefreshMetadata = true;
series.CoverImage = null; series.CoverImage = null;
series.CoverImageLocked = false; series.CoverImageLocked = false;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers);
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id);
series.ResetColorScape(); series.ResetColorScape();

View File

@ -6,6 +6,7 @@ using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs.Uploads; using API.DTOs.Uploads;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.MetadataMatching;
using API.Extensions; using API.Extensions;
using API.Services; using API.Services;
using API.Services.Tasks.Metadata; using API.Services.Tasks.Metadata;
@ -112,8 +113,10 @@ public class UploadController : BaseApiController
series.CoverImage = filePath; series.CoverImage = filePath;
series.CoverImageLocked = lockState; series.CoverImageLocked = lockState;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers);
_imageService.UpdateColorScape(series); _imageService.UpdateColorScape(series);
_unitOfWork.SeriesRepository.Update(series); _unitOfWork.SeriesRepository.Update(series);
_unitOfWork.SeriesRepository.Update(series.Metadata);
if (_unitOfWork.HasChanges()) if (_unitOfWork.HasChanges())
{ {
@ -277,6 +280,7 @@ public class UploadController : BaseApiController
chapter.CoverImage = filePath; chapter.CoverImage = filePath;
chapter.CoverImageLocked = lockState; chapter.CoverImageLocked = lockState;
chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterCovers);
_unitOfWork.ChapterRepository.Update(chapter); _unitOfWork.ChapterRepository.Update(chapter);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
if (volume != null) if (volume != null)

View File

@ -4,7 +4,6 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.KavitaPlus.Metadata;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Enums.UserPreferences; using API.Entities.Enums.UserPreferences;
@ -18,7 +17,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Data; namespace API.Data;
@ -43,7 +41,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ServerSetting> ServerSetting { get; set; } = null!; public DbSet<ServerSetting> ServerSetting { get; set; } = null!;
public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!; public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!;
public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!; public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!;
[Obsolete] [Obsolete("Use AppUserCollection")]
public DbSet<CollectionTag> CollectionTag { get; set; } = null!; public DbSet<CollectionTag> CollectionTag { get; set; } = null!;
public DbSet<AppUserBookmark> AppUserBookmark { get; set; } = null!; public DbSet<AppUserBookmark> AppUserBookmark { get; set; } = null!;
public DbSet<ReadingList> ReadingList { get; set; } = null!; public DbSet<ReadingList> ReadingList { get; set; } = null!;
@ -72,7 +70,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ExternalSeriesMetadata> ExternalSeriesMetadata { get; set; } = null!; public DbSet<ExternalSeriesMetadata> ExternalSeriesMetadata { get; set; } = null!;
public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!; public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!;
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!; public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
[Obsolete] [Obsolete("Use IsBlacklisted field on Series")]
public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!; public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!;
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!; public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!; public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!;
@ -286,6 +284,22 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<List<int>>(v, JsonSerializerOptions.Default) ?? new List<int>()) v => JsonSerializer.Deserialize<List<int>>(v, JsonSerializerOptions.Default) ?? new List<int>())
.HasColumnType("TEXT"); .HasColumnType("TEXT");
builder.Entity<SeriesMetadata>()
.Property(sm => sm.KPlusOverrides)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<IList<MetadataSettingField>>(v, JsonSerializerOptions.Default) ??
new List<MetadataSettingField>())
.HasColumnType("TEXT")
.HasDefaultValue(new List<MetadataSettingField>());
builder.Entity<Chapter>()
.Property(sm => sm.KPlusOverrides)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<IList<MetadataSettingField>>(v, JsonSerializerOptions.Default) ?? new List<MetadataSettingField>())
.HasColumnType("TEXT")
.HasDefaultValue(new List<MetadataSettingField>());
} }
#nullable enable #nullable enable

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class TrackKavitaPlusMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "KPlusOverrides",
table: "SeriesMetadata",
type: "TEXT",
nullable: true,
defaultValue: "[]");
migrationBuilder.AddColumn<string>(
name: "KPlusOverrides",
table: "Chapter",
type: "TEXT",
nullable: true,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "KPlusOverrides",
table: "SeriesMetadata");
migrationBuilder.DropColumn(
name: "KPlusOverrides",
table: "Chapter");
}
}
}

View File

@ -1,6 +1,8 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using API.Data; using API.Data;
using API.Entities.MetadataMatching;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -957,6 +959,11 @@ namespace API.Data.Migrations
b.Property<bool>("IsSpecial") b.Property<bool>("IsSpecial")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("KPlusOverrides")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("[]");
b.Property<string>("Language") b.Property<string>("Language")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -1683,6 +1690,11 @@ namespace API.Data.Migrations
b.Property<bool>("InkerLocked") b.Property<bool>("InkerLocked")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("KPlusOverrides")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("[]");
b.Property<string>("Language") b.Property<string>("Language")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@ -387,8 +387,8 @@ public class PersonRepository : IPersonRepository
return await _context.Person return await _context.Person
.Includes(includes) .Includes(includes)
.Where(p => EF.Functions.Like(p.Name, $"%{searchQuery}%") .Where(p => EF.Functions.Like(p.NormalizedName, $"%{searchQuery}%")
|| p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%"))) || p.Aliases.Any(pa => EF.Functions.Like(pa.NormalizedAlias, $"%{searchQuery}%")))
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider) .ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
} }

View File

@ -82,6 +82,7 @@ public interface ISeriesRepository
void Attach(Series series); void Attach(Series series);
void Attach(SeriesRelation relation); void Attach(SeriesRelation relation);
void Update(Series series); void Update(Series series);
void Update(SeriesMetadata seriesMetadata);
void Remove(Series series); void Remove(Series series);
void Remove(IEnumerable<Series> series); void Remove(IEnumerable<Series> series);
void Detach(Series series); void Detach(Series series);
@ -219,6 +220,11 @@ public class SeriesRepository : ISeriesRepository
_context.Entry(series).State = EntityState.Modified; _context.Entry(series).State = EntityState.Modified;
} }
public void Update(SeriesMetadata seriesMetadata)
{
_context.Entry(seriesMetadata).State = EntityState.Modified;
}
public void Remove(Series series) public void Remove(Series series)
{ {
_context.Series.Remove(series); _context.Series.Remove(series);

View File

@ -4,13 +4,14 @@ using System.Globalization;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;
using API.Entities.Metadata; using API.Entities.Metadata;
using API.Entities.MetadataMatching;
using API.Entities.Person; using API.Entities.Person;
using API.Extensions; using API.Extensions;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
namespace API.Entities; namespace API.Entities;
public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata
{ {
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
@ -126,6 +127,11 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
public string WebLinks { get; set; } = string.Empty; public string WebLinks { get; set; } = string.Empty;
public string ISBN { get; set; } = string.Empty; public string ISBN { get; set; } = string.Empty;
/// <summary>
/// Tracks which metadata has been set by K+
/// </summary>
public IList<MetadataSettingField> KPlusOverrides { get; set; } = [];
/// <summary> /// <summary>
/// (Kavita+) Average rating from Kavita+ metadata /// (Kavita+) Average rating from Kavita+ metadata
/// </summary> /// </summary>

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using API.Entities.MetadataMatching;
namespace API.Entities.Interfaces;
public interface IHasKPlusMetadata
{
/// <summary>
/// Tracks which metadata has been set by K+
/// </summary>
public IList<MetadataSettingField> KPlusOverrides { get; set; }
}

View File

@ -4,13 +4,14 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;
using API.Entities.MetadataMatching;
using API.Entities.Person; using API.Entities.Person;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Entities.Metadata; namespace API.Entities.Metadata;
[Index(nameof(Id), nameof(SeriesId), IsUnique = true)] [Index(nameof(Id), nameof(SeriesId), IsUnique = true)]
public class SeriesMetadata : IHasConcurrencyToken public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata
{ {
public int Id { get; set; } public int Id { get; set; }
@ -42,6 +43,10 @@ public class SeriesMetadata : IHasConcurrencyToken
/// </summary> /// </summary>
/// <remarks>This is not populated from Chapters of the Series</remarks> /// <remarks>This is not populated from Chapters of the Series</remarks>
public string WebLinks { get; set; } = string.Empty; public string WebLinks { get; set; } = string.Empty;
/// <summary>
/// Tracks which metadata has been set by K+
/// </summary>
public IList<MetadataSettingField> KPlusOverrides { get; set; } = [];
#region Locks #region Locks

View File

@ -0,0 +1,21 @@
using API.Entities.Interfaces;
using API.Entities.MetadataMatching;
namespace API.Extensions;
public static class IHasKPlusMetadataExtensions
{
public static bool HasSetKPlusMetadata(this IHasKPlusMetadata hasKPlusMetadata, MetadataSettingField field)
{
return hasKPlusMetadata.KPlusOverrides.Contains(field);
}
public static void AddKPlusOverride(this IHasKPlusMetadata hasKPlusMetadata, MetadataSettingField field)
{
if (hasKPlusMetadata.KPlusOverrides.Contains(field)) return;
hasKPlusMetadata.KPlusOverrides.Add(field);
}
}

View File

@ -2,6 +2,7 @@ using System.Threading.Tasks;
using API.Data; using API.Data;
using API.DTOs.Koreader; using API.DTOs.Koreader;
using API.DTOs.Progress; using API.DTOs.Progress;
using API.Extensions;
using API.Helpers; using API.Helpers;
using API.Helpers.Builders; using API.Helpers.Builders;
using Kavita.Common; using Kavita.Common;
@ -39,7 +40,7 @@ public class KoreaderService : IKoreaderService
/// <param name="userId"></param> /// <param name="userId"></param>
public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId) public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId)
{ {
_logger.LogDebug("Saving Koreader progress for {UserId}: {KoreaderProgress}", userId, koreaderBookDto.Progress); _logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.Progress.Sanitize());
var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document); var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document);
if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing"));

View File

@ -16,6 +16,7 @@ using API.DTOs.Scrobbling;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces;
using API.Entities.Metadata; using API.Entities.Metadata;
using API.Entities.MetadataMatching; using API.Entities.MetadataMatching;
using API.Entities.Person; using API.Entities.Person;
@ -526,6 +527,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (madeMetadataModification) if (madeMetadataModification)
{ {
_unitOfWork.SeriesRepository.Update(series); _unitOfWork.SeriesRepository.Update(series);
_unitOfWork.SeriesRepository.Update(series.Metadata);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -782,7 +784,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (externalCharacters == null || externalCharacters.Count == 0) return false; if (externalCharacters == null || externalCharacters.Count == 0) return false;
if (series.Metadata.CharacterLocked && !settings.HasOverride(MetadataSettingField.People)) if (series.Metadata.CharacterLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People))
{ {
return false; return false;
} }
@ -849,7 +851,7 @@ public class ExternalMetadataService : IExternalMetadataService
} }
} }
series.Metadata.AddKPlusOverride(MetadataSettingField.People);
return true; return true;
} }
@ -864,7 +866,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (upstreamArtists.Count == 0) return false; if (upstreamArtists.Count == 0) return false;
if (series.Metadata.CoverArtistLocked && !settings.HasOverride(MetadataSettingField.People)) if (series.Metadata.CoverArtistLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People))
{ {
return false; return false;
} }
@ -907,6 +909,7 @@ public class ExternalMetadataService : IExternalMetadataService
await DownloadAndSetPersonCovers(upstreamArtists); await DownloadAndSetPersonCovers(upstreamArtists);
series.Metadata.AddKPlusOverride(MetadataSettingField.People);
return true; return true;
} }
@ -920,7 +923,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (upstreamWriters.Count == 0) return false; if (upstreamWriters.Count == 0) return false;
if (series.Metadata.WriterLocked && !settings.HasOverride(MetadataSettingField.People)) if (series.Metadata.WriterLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People))
{ {
return false; return false;
} }
@ -963,7 +966,7 @@ public class ExternalMetadataService : IExternalMetadataService
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
await DownloadAndSetPersonCovers(upstreamWriters); await DownloadAndSetPersonCovers(upstreamWriters);
series.Metadata.AddKPlusOverride(MetadataSettingField.People);
return true; return true;
} }
@ -973,7 +976,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (!settings.EnableTags || processedTags.Count == 0) return false; if (!settings.EnableTags || processedTags.Count == 0) return false;
if (series.Metadata.TagsLocked && !settings.HasOverride(MetadataSettingField.Tags)) if (series.Metadata.TagsLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Tags))
{ {
return false; return false;
} }
@ -990,6 +993,11 @@ public class ExternalMetadataService : IExternalMetadataService
madeModification = true; madeModification = true;
}, () => series.Metadata.TagsLocked = true); }, () => series.Metadata.TagsLocked = true);
if (madeModification)
{
series.Metadata.AddKPlusOverride(MetadataSettingField.Tags);
}
return madeModification; return madeModification;
} }
@ -1014,7 +1022,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (!settings.EnableGenres || processedGenres.Count == 0) return false; if (!settings.EnableGenres || processedGenres.Count == 0) return false;
if (series.Metadata.GenresLocked && !settings.HasOverride(MetadataSettingField.Genres)) if (series.Metadata.GenresLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Genres))
{ {
return false; return false;
} }
@ -1035,6 +1043,12 @@ public class ExternalMetadataService : IExternalMetadataService
{ {
if (series.Metadata.Genres.FirstOrDefault(g => g.NormalizedTitle == genre.NormalizedTitle) != null) continue; if (series.Metadata.Genres.FirstOrDefault(g => g.NormalizedTitle == genre.NormalizedTitle) != null) continue;
series.Metadata.Genres.Add(genre); series.Metadata.Genres.Add(genre);
madeModification = true;
}
if (madeModification)
{
series.Metadata.AddKPlusOverride(MetadataSettingField.Genres);
} }
return madeModification; return madeModification;
@ -1044,7 +1058,7 @@ public class ExternalMetadataService : IExternalMetadataService
{ {
if (!settings.EnablePublicationStatus) return false; if (!settings.EnablePublicationStatus) return false;
if (series.Metadata.PublicationStatusLocked && !settings.HasOverride(MetadataSettingField.PublicationStatus)) if (series.Metadata.PublicationStatusLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.PublicationStatus))
{ {
return false; return false;
} }
@ -1058,6 +1072,7 @@ public class ExternalMetadataService : IExternalMetadataService
series.Metadata.PublicationStatus = status; series.Metadata.PublicationStatus = status;
series.Metadata.PublicationStatusLocked = true; series.Metadata.PublicationStatusLocked = true;
series.Metadata.AddKPlusOverride(MetadataSettingField.PublicationStatus);
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@ -1071,7 +1086,7 @@ public class ExternalMetadataService : IExternalMetadataService
private bool UpdateAgeRating(Series series, MetadataSettingsDto settings, IEnumerable<string> allExternalTags) private bool UpdateAgeRating(Series series, MetadataSettingsDto settings, IEnumerable<string> allExternalTags)
{ {
if (series.Metadata.AgeRatingLocked && !settings.HasOverride(MetadataSettingField.AgeRating)) if (series.Metadata.AgeRatingLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.AgeRating))
{ {
return false; return false;
} }
@ -1087,6 +1102,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (series.Metadata.AgeRating <= ageRating) if (series.Metadata.AgeRating <= ageRating)
{ {
series.Metadata.AgeRating = ageRating; series.Metadata.AgeRating = ageRating;
series.Metadata.AddKPlusOverride(MetadataSettingField.AgeRating);
return true; return true;
} }
} }
@ -1127,7 +1143,10 @@ public class ExternalMetadataService : IExternalMetadataService
madeModification = UpdateChapterTitle(chapter, settings, potentialMatch.Title, series.Name) || madeModification; madeModification = UpdateChapterTitle(chapter, settings, potentialMatch.Title, series.Name) || madeModification;
madeModification = UpdateChapterSummary(chapter, settings, potentialMatch.Summary) || madeModification; madeModification = UpdateChapterSummary(chapter, settings, potentialMatch.Summary) || madeModification;
madeModification = UpdateChapterReleaseDate(chapter, settings, potentialMatch.ReleaseDate) || madeModification; madeModification = UpdateChapterReleaseDate(chapter, settings, potentialMatch.ReleaseDate) || madeModification;
madeModification = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher) || madeModification;
var hasUpdatedPublisher = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher);
if (hasUpdatedPublisher) chapter.AddKPlusOverride(MetadataSettingField.ChapterPublisher);
madeModification = hasUpdatedPublisher || madeModification;
madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification; madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification;
madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification;
@ -1245,17 +1264,18 @@ public class ExternalMetadataService : IExternalMetadataService
if (string.IsNullOrEmpty(summary)) return false; if (string.IsNullOrEmpty(summary)) return false;
if (chapter.SummaryLocked && !settings.HasOverride(MetadataSettingField.ChapterSummary)) if (chapter.SummaryLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterSummary))
{ {
return false; return false;
} }
if (!string.IsNullOrWhiteSpace(summary) && !settings.HasOverride(MetadataSettingField.ChapterSummary)) if (!string.IsNullOrWhiteSpace(summary) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterSummary))
{ {
return false; return false;
} }
chapter.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(summary)); chapter.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(summary));
chapter.AddKPlusOverride(MetadataSettingField.ChapterSummary);
return true; return true;
} }
@ -1265,17 +1285,18 @@ public class ExternalMetadataService : IExternalMetadataService
if (string.IsNullOrEmpty(title)) return false; if (string.IsNullOrEmpty(title)) return false;
if (chapter.TitleNameLocked && !settings.HasOverride(MetadataSettingField.ChapterTitle)) if (chapter.TitleNameLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterTitle))
{ {
return false; return false;
} }
if (!title.Contains(seriesName) && !settings.HasOverride(MetadataSettingField.ChapterTitle)) if (!title.Contains(seriesName) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterTitle))
{ {
return false; return false;
} }
chapter.TitleName = title; chapter.TitleName = title;
chapter.AddKPlusOverride(MetadataSettingField.ChapterTitle);
return true; return true;
} }
@ -1285,17 +1306,18 @@ public class ExternalMetadataService : IExternalMetadataService
if (releaseDate == null || releaseDate == DateTime.MinValue) return false; if (releaseDate == null || releaseDate == DateTime.MinValue) return false;
if (chapter.ReleaseDateLocked && !settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) if (chapter.ReleaseDateLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterReleaseDate))
{ {
return false; return false;
} }
if (!settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) if (!HasForceOverride(settings, chapter, MetadataSettingField.ChapterReleaseDate))
{ {
return false; return false;
} }
chapter.ReleaseDate = releaseDate.Value; chapter.ReleaseDate = releaseDate.Value;
chapter.AddKPlusOverride(MetadataSettingField.ChapterReleaseDate);
return true; return true;
} }
@ -1305,12 +1327,12 @@ public class ExternalMetadataService : IExternalMetadataService
if (string.IsNullOrEmpty(publisher)) return false; if (string.IsNullOrEmpty(publisher)) return false;
if (chapter.PublisherLocked && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) if (chapter.PublisherLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterPublisher))
{ {
return false; return false;
} }
if (!string.IsNullOrWhiteSpace(publisher) && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) if (!string.IsNullOrWhiteSpace(publisher) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterPublisher))
{ {
return false; return false;
} }
@ -1332,7 +1354,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (string.IsNullOrEmpty(coverUrl)) return false; if (string.IsNullOrEmpty(coverUrl)) return false;
if (chapter.CoverImageLocked && !settings.HasOverride(MetadataSettingField.ChapterCovers)) if (chapter.CoverImageLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterCovers))
{ {
return false; return false;
} }
@ -1343,6 +1365,7 @@ public class ExternalMetadataService : IExternalMetadataService
} }
await DownloadChapterCovers(chapter, coverUrl); await DownloadChapterCovers(chapter, coverUrl);
chapter.AddKPlusOverride(MetadataSettingField.ChapterCovers);
return true; return true;
} }
@ -1352,7 +1375,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (staff?.Count == 0) return false; if (staff?.Count == 0) return false;
if (chapter.IsPersonRoleLocked(role) && !settings.HasOverride(MetadataSettingField.People)) if (chapter.IsPersonRoleLocked(role) && !HasForceOverride(settings, chapter, MetadataSettingField.People))
{ {
return false; return false;
} }
@ -1401,7 +1424,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (string.IsNullOrEmpty(externalMetadata.CoverUrl)) return false; if (string.IsNullOrEmpty(externalMetadata.CoverUrl)) return false;
if (series.CoverImageLocked && !settings.HasOverride(MetadataSettingField.Covers)) if (series.CoverImageLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Covers))
{ {
return false; return false;
} }
@ -1412,6 +1435,7 @@ public class ExternalMetadataService : IExternalMetadataService
} }
await DownloadSeriesCovers(series, externalMetadata.CoverUrl); await DownloadSeriesCovers(series, externalMetadata.CoverUrl);
series.Metadata.AddKPlusOverride(MetadataSettingField.Covers);
return true; return true;
} }
@ -1422,17 +1446,18 @@ public class ExternalMetadataService : IExternalMetadataService
if (!externalMetadata.StartDate.HasValue) return false; if (!externalMetadata.StartDate.HasValue) return false;
if (series.Metadata.ReleaseYearLocked && !settings.HasOverride(MetadataSettingField.StartDate)) if (series.Metadata.ReleaseYearLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.StartDate))
{ {
return false; return false;
} }
if (series.Metadata.ReleaseYear != 0 && !settings.HasOverride(MetadataSettingField.StartDate)) if (series.Metadata.ReleaseYear != 0 && !HasForceOverride(settings, series.Metadata, MetadataSettingField.StartDate))
{ {
return false; return false;
} }
series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year; series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year;
series.Metadata.AddKPlusOverride(MetadataSettingField.StartDate);
return true; return true;
} }
@ -1440,12 +1465,12 @@ public class ExternalMetadataService : IExternalMetadataService
{ {
if (!settings.EnableLocalizedName) return false; if (!settings.EnableLocalizedName) return false;
if (series.LocalizedNameLocked && !settings.HasOverride(MetadataSettingField.LocalizedName)) if (series.LocalizedNameLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.LocalizedName))
{ {
return false; return false;
} }
if (!string.IsNullOrWhiteSpace(series.LocalizedName) && !settings.HasOverride(MetadataSettingField.LocalizedName)) if (!string.IsNullOrWhiteSpace(series.LocalizedName) && !HasForceOverride(settings, series.Metadata, MetadataSettingField.LocalizedName))
{ {
return false; return false;
} }
@ -1471,6 +1496,7 @@ public class ExternalMetadataService : IExternalMetadataService
} }
series.Metadata.AddKPlusOverride(MetadataSettingField.LocalizedName);
return true; return true;
} }
@ -1480,17 +1506,18 @@ public class ExternalMetadataService : IExternalMetadataService
if (string.IsNullOrEmpty(externalMetadata.Summary)) return false; if (string.IsNullOrEmpty(externalMetadata.Summary)) return false;
if (series.Metadata.SummaryLocked && !settings.HasOverride(MetadataSettingField.Summary)) if (series.Metadata.SummaryLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Summary))
{ {
return false; return false;
} }
if (!string.IsNullOrWhiteSpace(series.Metadata.Summary) && !settings.HasOverride(MetadataSettingField.Summary)) if (!string.IsNullOrWhiteSpace(series.Metadata.Summary) && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Summary))
{ {
return false; return false;
} }
series.Metadata.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(externalMetadata.Summary)); series.Metadata.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(externalMetadata.Summary));
series.Metadata.AddKPlusOverride(MetadataSettingField.Summary);
return true; return true;
} }
@ -1509,7 +1536,8 @@ public class ExternalMetadataService : IExternalMetadataService
{ {
try try
{ {
await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false, true); // Only choose the better image if we're overriding a user provided cover
await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false, !series.Metadata.HasSetKPlusMetadata(MetadataSettingField.Covers));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1881,4 +1909,10 @@ public class ExternalMetadataService : IExternalMetadataService
return null; return null;
} }
private static bool HasForceOverride(MetadataSettingsDto settings, IHasKPlusMetadata kPlusMetadata,
MetadataSettingField field)
{
return settings.HasOverride(field) || kPlusMetadata.HasSetKPlusMetadata(field);
}
} }

View File

@ -11,7 +11,9 @@ using API.DTOs.Person;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces;
using API.Entities.Metadata; using API.Entities.Metadata;
using API.Entities.MetadataMatching;
using API.Entities.Person; using API.Entities.Person;
using API.Extensions; using API.Extensions;
using API.Helpers; using API.Helpers;
@ -120,23 +122,27 @@ public class SeriesService : ISeriesService
{ {
series.Metadata.ReleaseYear = updateSeriesMetadataDto.SeriesMetadata.ReleaseYear; series.Metadata.ReleaseYear = updateSeriesMetadataDto.SeriesMetadata.ReleaseYear;
series.Metadata.ReleaseYearLocked = true; series.Metadata.ReleaseYearLocked = true;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.StartDate);
} }
if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus) if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus)
{ {
series.Metadata.PublicationStatus = updateSeriesMetadataDto.SeriesMetadata.PublicationStatus; series.Metadata.PublicationStatus = updateSeriesMetadataDto.SeriesMetadata.PublicationStatus;
series.Metadata.PublicationStatusLocked = true; series.Metadata.PublicationStatusLocked = true;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.PublicationStatus);
} }
if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary)) if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary))
{ {
updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty; updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Summary);
} }
if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim()) if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim())
{ {
series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim() ?? string.Empty; series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim() ?? string.Empty;
series.Metadata.SummaryLocked = true; series.Metadata.SummaryLocked = true;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Summary);
} }
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language) if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language)
@ -195,6 +201,7 @@ public class SeriesService : ISeriesService
series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata?.AgeRating ?? AgeRating.Unknown; series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata?.AgeRating ?? AgeRating.Unknown;
series.Metadata.AgeRatingLocked = true; series.Metadata.AgeRatingLocked = true;
await _readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); await _readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating);
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating);
} }
else else
{ {
@ -206,6 +213,7 @@ public class SeriesService : ISeriesService
if (updatedRating > series.Metadata.AgeRating) if (updatedRating > series.Metadata.AgeRating)
{ {
series.Metadata.AgeRating = updatedRating; series.Metadata.AgeRating = updatedRating;
series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating);
} }
} }
} }
@ -319,6 +327,7 @@ public class SeriesService : ISeriesService
return true; return true;
} }
_unitOfWork.SeriesRepository.Update(series.Metadata);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
// Trigger code to clean up tags, collections, people, etc // Trigger code to clean up tags, collections, people, etc

View File

@ -81,10 +81,6 @@ export class SeriesService {
return this.httpClient.post<string>(this.baseUrl + 'series/delete-multiple', {seriesIds}, TextResonse).pipe(map(s => s === "true")); return this.httpClient.post<string>(this.baseUrl + 'series/delete-multiple', {seriesIds}, TextResonse).pipe(map(s => s === "true"));
} }
updateRating(seriesId: number, userRating: number) {
return this.httpClient.post(this.baseUrl + 'series/update-rating', {seriesId, userRating});
}
updateSeries(model: any) { updateSeries(model: any) {
return this.httpClient.post(this.baseUrl + 'series/update', model); return this.httpClient.post(this.baseUrl + 'series/update', model);
} }

View File

@ -78,6 +78,7 @@ export class ExternalRatingComponent implements OnInit {
modalRef.componentInstance.userRating = this.userRating; modalRef.componentInstance.userRating = this.userRating;
modalRef.componentInstance.seriesId = this.seriesId; modalRef.componentInstance.seriesId = this.seriesId;
modalRef.componentInstance.hasUserRated = this.hasUserRated; modalRef.componentInstance.hasUserRated = this.hasUserRated;
modalRef.componentInstance.chapterId = this.chapterId;
modalRef.closed.subscribe((updated: {hasUserRated: boolean, userRating: number}) => { modalRef.closed.subscribe((updated: {hasUserRated: boolean, userRating: number}) => {
this.userRating = updated.userRating; this.userRating = updated.userRating;

View File

@ -5,6 +5,7 @@ import {Breakpoint} from "../../../shared/_services/utility.service";
import {NgxStarsModule} from "ngx-stars"; import {NgxStarsModule} from "ngx-stars";
import {ThemeService} from "../../../_services/theme.service"; import {ThemeService} from "../../../_services/theme.service";
import {SeriesService} from "../../../_services/series.service"; import {SeriesService} from "../../../_services/series.service";
import {ReviewService} from "../../../_services/review.service";
@Component({ @Component({
selector: 'app-rating-modal', selector: 'app-rating-modal',
@ -20,7 +21,7 @@ export class RatingModalComponent {
protected readonly modal = inject(NgbActiveModal); protected readonly modal = inject(NgbActiveModal);
protected readonly themeService = inject(ThemeService); protected readonly themeService = inject(ThemeService);
protected readonly seriesService = inject(SeriesService); protected readonly reviewService = inject(ReviewService);
protected readonly cdRef = inject(ChangeDetectorRef); protected readonly cdRef = inject(ChangeDetectorRef);
protected readonly Breakpoint = Breakpoint; protected readonly Breakpoint = Breakpoint;
@ -28,14 +29,16 @@ export class RatingModalComponent {
@Input({required: true}) userRating!: number; @Input({required: true}) userRating!: number;
@Input({required: true}) seriesId!: number; @Input({required: true}) seriesId!: number;
@Input({required: true}) hasUserRated!: boolean; @Input({required: true}) hasUserRated!: boolean;
@Input() chapterId: number | undefined;
starColor = this.themeService.getCssVariable('--rating-star-color'); starColor = this.themeService.getCssVariable('--rating-star-color');
updateRating(rating: number) { updateRating(rating: number) {
this.seriesService.updateRating(this.seriesId, rating).subscribe(() => { this.reviewService.updateRating(this.seriesId, rating, this.chapterId).subscribe(() => {
this.userRating = rating; this.userRating = rating;
this.hasUserRated = true; this.hasUserRated = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.close();
}); });
} }

View File

@ -190,7 +190,7 @@ export class FilterUtilitiesService {
switch (type) { switch (type) {
case 'series': case 'series':
return [ return [
FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath, PersonFilterField.Name FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath
] as unknown as T[]; ] as unknown as T[];
case 'person': case 'person':
return [ return [