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

@ -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);
}