This commit is contained in:
Joe Milazzo 2025-03-02 17:55:23 -06:00 committed by GitHub
parent 78a98d0d18
commit 5af851af08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 264 additions and 117 deletions

View File

@ -21,6 +21,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_GHA_PAT }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
@ -59,7 +60,12 @@ jobs:
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
# Pull latest changes with rebase to avoid merge commits
git pull --rebase origin develop
# Commit and push
git commit -m "Update OpenAPI documentation" openapi.json
git push
env:
GITHUB_TOKEN: ${{ secrets.REPO_GHA_PAT }}
GITHUB_TOKEN: ${{ secrets.REPO_GHA_PAT }}

View File

@ -298,38 +298,38 @@ public class ScannerServiceTests : AbstractDbTest
}
[Fact]
public async Task ScanLibrary_PublishersInheritFromChapters()
{
const string testcase = "Flat Special - Manga.json";
var infos = new Dictionary<string, ComicInfo>();
infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo()
[Fact]
public async Task ScanLibrary_PublishersInheritFromChapters()
{
Publisher = "Correct Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo()
{
Publisher = "Special Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo()
{
Publisher = "Chapter Publisher"
});
const string testcase = "Flat Special - Manga.json";
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
var infos = new Dictionary<string, ComicInfo>();
infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo()
{
Publisher = "Correct Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo()
{
Publisher = "Special Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo()
{
Publisher = "Chapter Publisher"
});
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id);
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id);
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
Assert.NotNull(postLib);
Assert.Single(postLib.Series);
var publishers = postLib.Series.First().Metadata.People
.Where(p => p.Role == PersonRole.Publisher);
Assert.Equal(3, publishers.Count());
}
Assert.NotNull(postLib);
Assert.Single(postLib.Series);
var publishers = postLib.Series.First().Metadata.People
.Where(p => p.Role == PersonRole.Publisher);
Assert.Equal(3, publishers.Count());
}
/// <summary>

View File

@ -12,11 +12,6 @@
<LangVersion>latestmajor</LangVersion>
</PropertyGroup>
<!-- Moved to GA -->
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
<!-- <Delete Files="../openapi.json" />-->
<!-- <Exec Command="swagger tofile &#45;&#45;output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
<!-- </Target>-->
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>

View File

@ -7,6 +7,7 @@ using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Plus;
using EasyCaching.Core;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -21,7 +22,8 @@ public class LicenseController(
ILogger<LicenseController> logger,
ILicenseService licenseService,
ILocalizationService localizationService,
ITaskScheduler taskScheduler)
ITaskScheduler taskScheduler,
IEasyCachingProviderFactory cachingProviderFactory)
: BaseApiController
{
/// <summary>
@ -32,8 +34,13 @@ public class LicenseController(
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
{
var result = await licenseService.HasActiveLicense(forceCheck);
if (result)
var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
var cacheValue = await licenseInfoProvider.GetAsync<bool>(LicenseService.CacheKey);
if (result && !cacheValue.IsNull && !cacheValue.Value)
{
await taskScheduler.ScheduleKavitaPlusTasks();
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using API.Entities.Enums;
using API.Entities.Interfaces;
using Microsoft.EntityFrameworkCore;
@ -101,4 +102,24 @@ public class SeriesMetadata : IHasConcurrencyToken
{
RowVersion++;
}
/// <summary>
/// Any People in this Role present
/// </summary>
/// <param name="role"></param>
/// <returns></returns>
public bool AnyOfRole(PersonRole role)
{
return People.Any(p => p.Role == role);
}
/// <summary>
/// Are all instances of the role from Kavita+
/// </summary>
/// <param name="role"></param>
/// <returns></returns>
public bool AllKavitaPlus(PersonRole role)
{
return People.Where(p => p.Role == role).All(p => p.KavitaPlusConnection);
}
}

View File

@ -295,7 +295,16 @@ public class ExternalMetadataService : IExternalMetadataService
if (data == null) return _defaultReturn;
// Get from Kavita+ API the Full Series metadata with rec/rev and cache to ExternalMetadata tables
return await FetchExternalMetadataForSeries(seriesId, libraryType, data);
try
{
return await FetchExternalMetadataForSeries(seriesId, libraryType, data);
}
catch (KavitaException ex)
{
_logger.LogError(ex, "Rate limit hit fetching metadata");
// This can happen when we hit rate limit
return _defaultReturn;
}
}
/// <summary>
@ -314,38 +323,49 @@ public class ExternalMetadataService : IExternalMetadataService
_unitOfWork.SeriesRepository.Update(series);
// Refetch metadata with a Direct lookup
var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, new PlusSeriesRequestDto()
try
{
AniListId = anilistId,
MalId = malId,
SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed
});
var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type,
new PlusSeriesRequestDto()
{
AniListId = anilistId,
MalId = malId,
SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed
});
if (metadata.Series == null)
{
_logger.LogError("Unable to Match {SeriesName} with Kavita+ Series AniList Id: {AniListId}", series.Name, anilistId);
return;
if (metadata.Series == null)
{
_logger.LogError("Unable to Match {SeriesName} with Kavita+ Series AniList Id: {AniListId}",
series.Name, anilistId);
return;
}
// Find all scrobble events and rewrite them to be the correct
var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId);
_unitOfWork.ScrobbleRepository.Remove(events);
// Find all scrobble errors and remove them
var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId);
_unitOfWork.ScrobbleRepository.Remove(errors);
await _unitOfWork.CommitAsync();
// Regenerate all events for the series for all users
BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistoryForSeries(seriesId));
// Name can be null on Series even with a direct match
_logger.LogInformation("Matched {SeriesName} with Kavita+ Series {MatchSeriesName}", series.Name,
metadata.Series.Name);
}
catch (KavitaException ex)
{
// We can't rethrow because Fix match is done in a background thread and Hangfire will requeue multiple times
_logger.LogInformation(ex, "Rate limit hit for matching {SeriesName} with Kavita+", series.Name);
}
// Find all scrobble events and rewrite them to be the correct
var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId);
_unitOfWork.ScrobbleRepository.Remove(events);
// Find all scrobble errors and remove them
var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId);
_unitOfWork.ScrobbleRepository.Remove(errors);
await _unitOfWork.CommitAsync();
// Regenerate all events for the series for all users
BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistoryForSeries(seriesId));
// Name can be null on Series even with a direct match
_logger.LogInformation("Matched {SeriesName} with Kavita+ Series {MatchSeriesName}", series.Name, metadata.Series.Name);
}
/// <summary>
/// Sets a series to Dont Match and removes all previously cached
/// Sets a series to Don't Match and removes all previously cached
/// </summary>
/// <param name="seriesId"></param>
public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch)
@ -383,7 +403,10 @@ public class ExternalMetadataService : IExternalMetadataService
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library);
if (series == null) return _defaultReturn;
if (series == null)
{
return _defaultReturn;
}
try
{
@ -417,7 +440,7 @@ public class ExternalMetadataService : IExternalMetadataService
// Recommendations
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
externalSeriesMetadata.ExternalRecommendations ??= [];
var recs = await ProcessRecommendations(libraryType, result.Recommendations, externalSeriesMetadata);
var extRatings = externalSeriesMetadata.ExternalRatings
@ -437,11 +460,19 @@ public class ExternalMetadataService : IExternalMetadataService
{
externalSeriesMetadata.Series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId);
if (madeMetadataModification)
try
{
_unitOfWork.SeriesRepository.Update(series);
madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId);
if (madeMetadataModification)
{
_unitOfWork.SeriesRepository.Update(series);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception when trying to write Series metadata from Kavita+");
}
}
// WriteExternalMetadataToSeries will commit but not always
@ -466,13 +497,27 @@ public class ExternalMetadataService : IExternalMetadataService
}
catch (FlurlHttpException ex)
{
var errorMessage = await ex.GetResponseStringAsync();
// Trim quotes if the response is a JSON string
errorMessage = errorMessage.Trim('"');
if (ex.StatusCode == 500)
{
return _defaultReturn;
}
if (ex.StatusCode == 400 && errorMessage.Contains("Too many Requests"))
{
throw new KavitaException("Too many requests, slow down");
}
}
catch (Exception ex)
{
if (ex.Message.Contains("Too Many Requests"))
{
throw new KavitaException("Too many requests, slow down");
}
_logger.LogError(ex, "Unable to fetch external series metadata from Kavita+");
}
@ -1079,10 +1124,18 @@ public class ExternalMetadataService : IExternalMetadataService
var aniListId = ScrobblingService.ExtractId<int?>(staff.Url, ScrobblingService.AniListStaffWebsite);
if (aniListId is null or <= 0) continue;
var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId.Value);
if (person != null && !string.IsNullOrEmpty(staff.ImageUrl) && string.IsNullOrEmpty(person.CoverImage))
if (person == null || string.IsNullOrEmpty(staff.ImageUrl) ||
!string.IsNullOrEmpty(person.CoverImage) || staff.ImageUrl.EndsWith("default.jpg")) continue;
try
{
await _coverDbService.SetPersonCoverByUrl(person, staff.ImageUrl, false, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception saving cover image for Person {PersonName} ({PersonId})", person.Name, person.Id);
}
}
}

View File

@ -44,7 +44,10 @@ public class LicenseService(
{
private readonly TimeSpan _licenseCacheTimeout = TimeSpan.FromHours(8);
public const string Cron = "0 */9 * * *";
private const string CacheKey = "license";
/// <summary>
/// Cache key for if license is valid or not
/// </summary>
public const string CacheKey = "license";
private const string LicenseInfoCacheKey = "license-info";

View File

@ -19,7 +19,6 @@ using API.SignalR;
using Flurl.Http;
using Hangfire;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -77,7 +76,7 @@ public class ScrobblingService : IScrobblingService
public const string AniListCharacterWebsite = "https://anilist.co/character/";
private static readonly IDictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>()
private static readonly Dictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>()
{
{AniListWeblinkWebsite, 0},
{MalWeblinkWebsite, 0},
@ -89,18 +88,14 @@ public class ScrobblingService : IScrobblingService
private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
private static readonly IList<ScrobbleProvider> BookProviders = new List<ScrobbleProvider>()
{
};
private static readonly IList<ScrobbleProvider> LightNovelProviders = new List<ScrobbleProvider>()
{
private static readonly IList<ScrobbleProvider> BookProviders = [];
private static readonly IList<ScrobbleProvider> LightNovelProviders =
[
ScrobbleProvider.AniList
};
private static readonly IList<ScrobbleProvider> ComicProviders = new List<ScrobbleProvider>();
private static readonly IList<ScrobbleProvider> MangaProviders = new List<ScrobbleProvider>()
{
ScrobbleProvider.AniList
};
];
private static readonly IList<ScrobbleProvider> ComicProviders = [];
private static readonly IList<ScrobbleProvider> MangaProviders = (List<ScrobbleProvider>)
[ScrobbleProvider.AniList];
private const string UnknownSeriesErrorMessage = "Series cannot be matched for Scrobbling";
@ -532,11 +527,10 @@ public class ScrobblingService : IScrobblingService
{
// Create a new ExternalMetadata entry to indicate that this is not matchable
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(evt.SeriesId, SeriesIncludes.ExternalMetadata);
if (series.ExternalSeriesMetadata == null)
{
series.ExternalSeriesMetadata = new ExternalSeriesMetadata() {SeriesId = evt.SeriesId};
}
series!.IsBlacklisted = true;
if (series == null) return 0;
series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() {SeriesId = evt.SeriesId};
series.IsBlacklisted = true;
_unitOfWork.SeriesRepository.Update(series);
_unitOfWork.ScrobbleRepository.Attach(new ScrobbleError()
@ -824,6 +818,7 @@ public class ScrobblingService : IScrobblingService
readEvt.AppUser.Id);
_unitOfWork.ScrobbleRepository.Update(readEvt);
}
progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, async evt => new ScrobbleDto()
{
Format = evt.Format,
@ -888,9 +883,9 @@ public class ScrobblingService : IScrobblingService
await _unitOfWork.CommitAsync();
}
}
catch (FlurlHttpException)
catch (FlurlHttpException ex)
{
_logger.LogError("Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data");
_logger.LogError(ex, "Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data");
return;
}
@ -986,7 +981,7 @@ public class ScrobblingService : IScrobblingService
{
if (ex.Message.Contains("Access token is invalid"))
{
_logger.LogCritical("Access Token for UserId: {UserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id);
_logger.LogCritical(ex, "Access Token for UserId: {UserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id);
evt.IsErrored = true;
evt.ErrorDetails = AccessTokenErrorMessage;
_unitOfWork.ScrobbleRepository.Update(evt);
@ -1106,7 +1101,7 @@ public class ScrobblingService : IScrobblingService
return null; // Unsupported website
}
if (id == null)
if (Equals(id, default(T)))
{
throw new ArgumentNullException(nameof(id), "ID cannot be null.");
}
@ -1140,7 +1135,7 @@ public class ScrobblingService : IScrobblingService
}
catch (Exception ex)
{
_logger.LogInformation("User {UserName} had an issue figuring out rate: {Message}", user.UserName, ex.Message);
_logger.LogInformation(ex, "User {UserName} had an issue figuring out rate: {Message}", user.UserName, ex.Message);
userRateLimits.Add(user.Id, 0);
}

View File

@ -331,84 +331,123 @@ public class ProcessSeries : IProcessSeries
}
#region PeopleAndTagsAndGenres
if (!series.Metadata.WriterLocked)
{
var personSw = Stopwatch.StartNew();
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Writer)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Writer);
_logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Series: {File} for {Count} people", personSw.ElapsedMilliseconds, series.Name, chapterPeople.Count);
}
if (!series.Metadata.WriterLocked)
{
var personSw = Stopwatch.StartNew();
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Writer)).ToList();
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Writer))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Writer);
}
_logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Series: {File} for {Count} people", personSw.ElapsedMilliseconds, series.Name, chapterPeople.Count);
}
if (!series.Metadata.ColoristLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Colorist)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Colorist);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Colorist))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Colorist);
}
}
if (!series.Metadata.PublisherLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Publisher)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Publisher);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Publisher))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Publisher);
}
}
if (!series.Metadata.CoverArtistLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.CoverArtist)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.CoverArtist);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.CoverArtist))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.CoverArtist);
}
}
if (!series.Metadata.CharacterLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Character)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Character);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Character))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Character);
}
}
if (!series.Metadata.EditorLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Editor)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Editor);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Editor))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Editor);
}
}
if (!series.Metadata.InkerLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Inker)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Inker);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Inker))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Inker);
}
}
if (!series.Metadata.ImprintLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Imprint)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Imprint);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Imprint))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Imprint);
}
}
if (!series.Metadata.TeamLocked)
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Team)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Team);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Team))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Team);
}
}
if (!series.Metadata.LocationLocked)
if (!series.Metadata.LocationLocked && !series.Metadata.AllKavitaPlus(PersonRole.Location))
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Location)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Location);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Location))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Location);
}
}
if (!series.Metadata.LettererLocked)
if (!series.Metadata.LettererLocked && !series.Metadata.AllKavitaPlus(PersonRole.Letterer))
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Letterer)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Letterer);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Location))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Letterer);
}
}
if (!series.Metadata.PencillerLocked)
if (!series.Metadata.PencillerLocked && !series.Metadata.AllKavitaPlus(PersonRole.Penciller))
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Penciller)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Penciller);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Penciller))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Penciller);
}
}
if (!series.Metadata.TranslatorLocked)
if (!series.Metadata.TranslatorLocked && !series.Metadata.AllKavitaPlus(PersonRole.Translator))
{
var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Translator)).ToList();
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Translator);
if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Translator))
{
await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Translator);
}
}
@ -428,6 +467,34 @@ public class ProcessSeries : IProcessSeries
}
/// <summary>
/// Ensure that we don't overwrite Person metadata when all metadata is coming from Kavita+ metadata match functionality
/// </summary>
/// <param name="series"></param>
/// <param name="chapterPeople"></param>
/// <param name="role"></param>
/// <returns></returns>
private static bool ShouldUpdatePeopleForRole(Series series, List<ChapterPeople> chapterPeople, PersonRole role)
{
if (chapterPeople.Count == 0) return false;
// If metadata already has this role, but all entries are from KavitaPlus, we should retain them
if (series.Metadata.AnyOfRole(role))
{
var existingPeople = series.Metadata.People.Where(p => p.Role == role);
// If all existing people are KavitaPlus but new chapter people exist, we should still update
if (existingPeople.All(p => p.KavitaPlusConnection))
{
return false; // Ensure we don't remove KavitaPlus people
}
return true; // Default case: metadata exists, and it's okay to update
}
return true;
}
private async Task UpdateCollectionTags(Series series, Chapter firstChapter)
{
// Get the default admin to associate these tags to
@ -553,8 +620,8 @@ public class ProcessSeries : IProcessSeries
.Where(v => v.MaxNumber.IsNot(Parser.Parser.SpecialVolumeNumber))
.ToList();
var maxVolume = (int) (nonSpecialVolumes.Any() ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0);
var maxChapter = (int) chapters.Max(c => c.MaxNumber);
var maxVolume = (int)(nonSpecialVolumes.Any() ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0);
var maxChapter = (int)chapters.Max(c => c.MaxNumber);
// Single books usually don't have a number in their Range (filename)
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)