Stability (I hope) (#2688)

This commit is contained in:
Joe Milazzo 2024-02-04 10:51:07 -06:00 committed by GitHub
parent 92ad7db918
commit 7e61cca92d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 3336 additions and 177 deletions

View file

@ -15,9 +15,8 @@ public static class EasyCacheProfiles
/// Cache the libraries on the server
/// </summary>
public const string Library = "library";
public const string KavitaPlusExternalSeries = "kavita+externalSeries";
/// <summary>
/// Series Detail page for Kavita+ stuff
/// External Series metadata for Kavita+ recommendation
/// </summary>
public const string KavitaPlusSeriesDetail = "kavita+seriesDetail";
public const string KavitaPlusExternalSeries = "kavita+externalSeries";
}

View file

@ -2,23 +2,18 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Misc;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Plus;
using EasyCaching.Core;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Mvc;
@ -26,11 +21,10 @@ namespace API.Controllers;
#nullable enable
public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService, ILicenseService licenseService,
IExternalMetadataService metadataService, IEasyCachingProviderFactory cachingProviderFactory)
public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService,
IExternalMetadataService metadataService)
: BaseApiController
{
private readonly IEasyCachingProvider _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusSeriesDetail);
public const string CacheKey = "kavitaPlusSeriesDetail_";
/// <summary>
@ -43,7 +37,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
if (ids is {Count: > 0})
{
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId()));
}
@ -61,7 +55,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(PersonRole? role)
{
return role.HasValue ?
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) :
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role.Value)) :
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
}
@ -75,7 +69,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
if (ids is {Count: > 0})
{
return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId()));
}
@ -92,7 +86,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
if (ids is {Count: > 0})
{
return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId()));
}
@ -110,7 +104,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
if (ids is {Count: > 0})
{
return Ok(await unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
}
@ -184,65 +178,45 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
[HttpGet("chapter-summary")]
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
{
// TODO: This doesn't seem used anywhere
if (chapterId <= 0) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
if (chapter == null) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
return Ok(chapter.Summary);
}
/// <summary>
/// If this Series is on Kavita+ Blacklist, removes it. If already cached, invalidates it.
/// This then attempts to refresh data from Kavita+ for this series.
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpPost("force-refresh")]
public async Task<ActionResult> ForceRefresh(int seriesId)
{
await metadataService.ForceKavitaPlusRefresh(seriesId);
return Ok();
}
/// <summary>
/// Fetches the details needed from Kavita+ for Series Detail page
/// </summary>
/// <remarks>This will hit upstream K+ if the data in local db is 2 weeks old</remarks>
/// <param name="seriesId"></param>
/// <param name="seriesId">Series Id</param>
/// <param name="libraryType">Library Type</param>
/// <returns></returns>
[HttpGet("series-detail-plus")]
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId, LibraryType libraryType, CancellationToken cancellationToken)
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId, LibraryType libraryType)
{
var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId()))
.Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0)
.ToList();
var cacheKey = CacheKey + seriesId;
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey, cancellationToken);
if (results.HasValue)
{
var cachedResult = results.Value;
await PrepareSeriesDetail(userReviews, cachedResult);
return cachedResult;
}
SeriesDetailPlusDto? ret = null;
if (ExternalMetadataService.IsPlusEligible(libraryType) && await licenseService.HasActiveLicense())
{
ret = await metadataService.GetSeriesDetailPlus(seriesId);
}
if (ret == null)
{
// Cache an empty result, so we don't constantly hit K+ when we know nothing is going to resolve
ret = new SeriesDetailPlusDto()
{
Reviews = new List<UserReviewDto>(),
Recommendations = null,
Ratings = null
};
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48), cancellationToken);
var newCacheResult2 = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult2);
return Ok(newCacheResult2);
}
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48), cancellationToken);
// For some reason if we don't use a different instance, the cache keeps changes made below
var newCacheResult = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey, cancellationToken)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult);
return Ok(newCacheResult);
var ret = await metadataService.GetSeriesDetailPlus(seriesId, libraryType);
await PrepareSeriesDetail(userReviews, ret);
return Ok(ret);
}
private async Task PrepareSeriesDetail(List<UserReviewDto> userReviews, SeriesDetailPlusDto ret)
@ -253,7 +227,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList()));
ret.Reviews = userReviews;
if (!isAdmin && ret.Recommendations != null)
if (!isAdmin && ret.Recommendations != null && user != null)
{
// Re-obtain owned series and take into account age restriction
ret.Recommendations.OwnedSeries =
@ -262,7 +236,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
}
if (ret.Recommendations != null)
if (ret.Recommendations != null && user != null)
{
ret.Recommendations.OwnedSeries ??= new List<SeriesDto>();
await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries);

View file

@ -270,8 +270,6 @@ public class ServerController : BaseApiController
_logger.LogInformation("Busting Kavita+ Cache");
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries);
await provider.FlushAsync();
provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusSeriesDetail);
await provider.FlushAsync();
return Ok();
}

View file

@ -63,6 +63,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ExternalSeriesMetadata> ExternalSeriesMetadata { get; set; } = null!;
public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!;
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class BlackListSeries : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "LastUpdatedUtc",
table: "ExternalSeriesMetadata",
newName: "ValidUntilUtc");
migrationBuilder.CreateTable(
name: "SeriesBlacklist",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
LastChecked = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SeriesBlacklist", x => x.Id);
table.ForeignKey(
name: "FK_SeriesBlacklist_Series_SeriesId",
column: x => x.SeriesId,
principalTable: "Series",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SeriesBlacklist_SeriesId",
table: "SeriesBlacklist",
column: "SeriesId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SeriesBlacklist");
migrationBuilder.RenameColumn(
name: "ValidUntilUtc",
table: "ExternalSeriesMetadata",
newName: "LastUpdatedUtc");
}
}
}

View file

@ -1178,15 +1178,15 @@ namespace API.Data.Migrations
b.Property<string>("GoogleBooksId")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("TEXT");
b.Property<long>("MalId")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<DateTime>("ValidUntilUtc")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SeriesId")
@ -1195,6 +1195,25 @@ namespace API.Data.Migrations
b.ToTable("ExternalSeriesMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastChecked")
.HasColumnType("TEXT");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SeriesId");
b.ToTable("SeriesBlacklist");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
{
b.Property<int>("Id")
@ -2393,6 +2412,17 @@ namespace API.Data.Migrations
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b =>
{
b.HasOne("API.Entities.Series", "Series")
.WithMany()
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
{
b.HasOne("API.Entities.Series", "Series")

View file

@ -27,9 +27,12 @@ public interface IExternalSeriesMetadataRepository
void Remove(IEnumerable<ExternalRating>? ratings);
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId, DateTime expireTime);
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
Task LinkRecommendationsToSeries(Series series);
Task<bool> IsBlacklistedSeries(int seriesId);
Task CreateBlacklistedSeries(int seriesId);
Task RemoveFromBlacklist(int seriesId);
}
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
@ -92,12 +95,12 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.FirstOrDefaultAsync();
}
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId, DateTime expireTime)
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
{
var row = await _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.FirstOrDefaultAsync();
return row == null || row.LastUpdatedUtc <= expireTime;
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
}
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
@ -184,4 +187,41 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
await _context.SaveChangesAsync();
}
public Task<bool> IsBlacklistedSeries(int seriesId)
{
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
}
/// <summary>
/// Creates a new instance against SeriesId and Saves to the DB
/// </summary>
/// <param name="seriesId"></param>
public async Task CreateBlacklistedSeries(int seriesId)
{
if (seriesId <= 0) return;
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
{
SeriesId = seriesId
});
await _context.SaveChangesAsync();
}
/// <summary>
/// Removes the Series from Blacklist and Saves to the DB
/// </summary>
/// <param name="seriesId"></param>
public async Task RemoveFromBlacklist(int seriesId)
{
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
if (seriesBlacklist != null)
{
// Remove the SeriesBlacklist entity from the context
_context.SeriesBlacklist.Remove(seriesBlacklist);
// Save the changes to the database
await _context.SaveChangesAsync();
}
}
}

View file

@ -43,6 +43,7 @@ public interface ILibraryRepository
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
IEnumerable<int> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None);
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
Task<LibraryType> GetLibraryTypeBySeriesIdAsync(int seriesId);
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None);
Task<int> GetTotalFiles();
IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId);
@ -54,6 +55,7 @@ public interface ILibraryRepository
Task<IList<string>> GetAllCoverImagesAsync();
Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
}
public class LibraryRepository : ILibraryRepository
@ -142,6 +144,14 @@ public class LibraryRepository : ILibraryRepository
.FirstAsync();
}
public async Task<LibraryType> GetLibraryTypeBySeriesIdAsync(int seriesId)
{
return await _context.Series
.Where(s => s.Id == seriesId)
.Select(s => s.Library.Type)
.FirstAsync();
}
public async Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None)
{
return await _context.Library

View file

@ -13,6 +13,7 @@ using API.DTOs.Filtering;
using API.DTOs.Filtering.v2;
using API.DTOs.Metadata;
using API.DTOs.ReadingLists;
using API.DTOs.Scrobbling;
using API.DTOs.Search;
using API.DTOs.SeriesDetail;
using API.DTOs.Settings;
@ -25,6 +26,7 @@ using API.Extensions.QueryExtensions.Filtering;
using API.Helpers;
using API.Helpers.Converters;
using API.Services;
using API.Services.Plus;
using API.Services.Tasks;
using API.Services.Tasks.Scanner;
using AutoMapper;
@ -151,7 +153,7 @@ public interface ISeriesRepository
Task RemoveFromOnDeck(int seriesId, int userId);
Task ClearOnDeckRemoval(int seriesId, int userId);
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto);
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
}
public class SeriesRepository : ISeriesRepository
@ -701,6 +703,30 @@ public class SeriesRepository : ISeriesRepository
return await PagedList<SeriesDto>.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
}
public async Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId)
{
return await _context.Series
.Where(s => s.Id == seriesId)
.Select(series => new PlusSeriesDto()
{
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
SeriesName = series.Name,
AltSeriesName = series.LocalizedName,
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
ScrobblingService.AniListWeblinkWebsite),
MalId = ScrobblingService.ExtractId<long?>(series.Metadata.WebLinks,
ScrobblingService.MalWeblinkWebsite),
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.GoogleBooksWeblinkWebsite),
MangaDexId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.MangaDexWeblinkWebsite),
VolumeCount = series.Volumes.Count,
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
Year = series.Metadata.ReleaseYear
})
.FirstOrDefaultAsync();
}
public async Task AddSeriesModifiers(int userId, IList<SeriesDto> series)
{
var userProgress = await _context.AppUserProgresses

View file

@ -29,7 +29,10 @@ public class ExternalSeriesMetadata
public long MalId { get; set; }
public string GoogleBooksId { get; set; }
public DateTime LastUpdatedUtc { get; set; }
/// <summary>
/// Data is valid until this time
/// </summary>
public DateTime ValidUntilUtc { get; set; }
public Series Series { get; set; } = null!;
public int SeriesId { get; set; }

View file

@ -0,0 +1,14 @@
using System;
namespace API.Entities.Metadata;
/// <summary>
/// A blacklist of Series for Kavita+
/// </summary>
public class SeriesBlacklist
{
public int Id { get; set; }
public int SeriesId { get; set; }
public Series Series { get; set; }
public DateTime LastChecked { get; set; } = DateTime.UtcNow;
}

View file

@ -86,12 +86,11 @@ public static class ApplicationServiceExtensions
// KavitaPlus stuff
options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries);
options.UseInMemory(EasyCacheProfiles.KavitaPlusSeriesDetail);
});
services.AddMemoryCache(options =>
{
options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 50 MB
options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB
options.CompactionPercentage = 0.1; // LRU compaction (10%)
});

View file

@ -14,7 +14,6 @@ public static class LibraryTypeHelper
LibraryType.Manga => MediaFormat.Manga,
LibraryType.Comic => MediaFormat.Comic,
LibraryType.Book => MediaFormat.LightNovel,
_ => throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null)
};
}
}

View file

@ -12,7 +12,6 @@ using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Extensions;
using API.Helpers.Builders;
using AutoMapper;
using Flurl.Http;
using Kavita.Common;
@ -48,7 +47,8 @@ internal class SeriesDetailPlusApiDto
public interface IExternalMetadataService
{
Task<ExternalSeriesDetailDto?> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId);
Task<SeriesDetailPlusDto?> GetSeriesDetailPlus(int seriesId);
Task<SeriesDetailPlusDto> GetSeriesDetailPlus(int seriesId, LibraryType libraryType);
Task ForceKavitaPlusRefresh(int seriesId);
}
public class ExternalMetadataService : IExternalMetadataService
@ -56,23 +56,54 @@ public class ExternalMetadataService : IExternalMetadataService
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ExternalMetadataService> _logger;
private readonly IMapper _mapper;
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(14);
private readonly ILicenseService _licenseService;
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30);
private readonly SeriesDetailPlusDto _defaultReturn = new()
{
Recommendations = null,
Ratings = ArraySegment<RatingDto>.Empty,
Reviews = ArraySegment<UserReviewDto>.Empty
};
public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger<ExternalMetadataService> logger, IMapper mapper)
public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger<ExternalMetadataService> logger, IMapper mapper, ILicenseService licenseService)
{
_unitOfWork = unitOfWork;
_logger = logger;
_mapper = mapper;
_licenseService = licenseService;
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
}
/// <summary>
/// Checks if the library type is allowed to interact with Kavita+
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsPlusEligible(LibraryType type)
{
return type != LibraryType.Comic;
}
/// <summary>
/// Removes from Blacklist and Invalidates the cache
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task ForceKavitaPlusRefresh(int seriesId)
{
if (!await _licenseService.HasActiveLicense()) return;
// Remove from Blacklist if applicable
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeBySeriesIdAsync(seriesId);
if (!IsPlusEligible(libraryType)) return;
await _unitOfWork.ExternalSeriesMetadataRepository.RemoveFromBlacklist(seriesId);
var metadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId);
if (metadata == null) return;
metadata.ValidUntilUtc = DateTime.UtcNow.Subtract(_externalSeriesMetadataCache);
await _unitOfWork.CommitAsync();
}
/// <summary>
/// Retrieves Metadata about a Recommended External Series
/// </summary>
@ -102,11 +133,15 @@ public class ExternalMetadataService : IExternalMetadataService
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlus(int seriesId)
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlus(int seriesId, LibraryType libraryType)
{
if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense()) return _defaultReturn;
// Check blacklist (bad matches)
if (await _unitOfWork.ExternalSeriesMetadataRepository.IsBlacklistedSeries(seriesId)) return _defaultReturn;
var needsRefresh =
await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId,
DateTime.UtcNow.Subtract(_externalSeriesMetadataCache));
await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId);
if (!needsRefresh)
{
@ -116,10 +151,8 @@ public class ExternalMetadataService : IExternalMetadataService
try
{
var series =
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Volumes | SeriesIncludes.Chapters);
if (series == null || series.Library.Type == LibraryType.Comic) return null;
var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId);
if (data == null) return _defaultReturn;
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
@ -130,12 +163,13 @@ public class ExternalMetadataService : IExternalMetadataService
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
.PostJsonAsync(data)
.ReceiveJson<SeriesDetailPlusApiDto>();
// Clear out existing results
var externalSeriesMetadata = await GetExternalSeriesMetadataForSeries(seriesId, series);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
var externalSeriesMetadata = await GetExternalSeriesMetadataForSeries(seriesId, series!);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations);
@ -157,19 +191,18 @@ public class ExternalMetadataService : IExternalMetadataService
// Recommendations
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
var recs = await ProcessRecommendations(series, result.Recommendations, externalSeriesMetadata);
var recs = await ProcessRecommendations(libraryType, result.Recommendations, externalSeriesMetadata);
var extRatings = externalSeriesMetadata.ExternalRatings
.Where(r => r.AverageScore > 0)
.ToList();
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow;
externalSeriesMetadata.ValidUntilUtc = DateTime.UtcNow.Add(_externalSeriesMetadataCache);
externalSeriesMetadata.AverageExternalRating = extRatings.Count != 0 ? (int) extRatings
.Average(r => r.AverageScore) : 0;
if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value;
if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value;
await _unitOfWork.CommitAsync();
return new SeriesDetailPlusDto()
@ -181,9 +214,9 @@ public class ExternalMetadataService : IExternalMetadataService
}
catch (FlurlHttpException ex)
{
if (ex.StatusCode == 404)
if (ex.StatusCode == 500)
{
return null;
return _defaultReturn;
}
}
catch (Exception ex)
@ -191,7 +224,10 @@ public class ExternalMetadataService : IExternalMetadataService
_logger.LogError(ex, "An error happened during the request to Kavita+ API");
}
return null;
// Blacklist the series as it wasn't found in Kavita+
await _unitOfWork.ExternalSeriesMetadataRepository.CreateBlacklistedSeries(seriesId);
return _defaultReturn;
}
@ -200,14 +236,16 @@ public class ExternalMetadataService : IExternalMetadataService
var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId);
if (externalSeriesMetadata != null) return externalSeriesMetadata;
externalSeriesMetadata = new ExternalSeriesMetadata();
externalSeriesMetadata = new ExternalSeriesMetadata()
{
SeriesId = seriesId,
};
series.ExternalSeriesMetadata = externalSeriesMetadata;
externalSeriesMetadata.SeriesId = series.Id;
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
return externalSeriesMetadata;
}
private async Task<RecommendationDto> ProcessRecommendations(Series series, IEnumerable<MediaRecommendationDto> recs,
private async Task<RecommendationDto> ProcessRecommendations(LibraryType libraryType, IEnumerable<MediaRecommendationDto> recs,
ExternalSeriesMetadata externalSeriesMetadata)
{
var recDto = new RecommendationDto()
@ -221,7 +259,7 @@ public class ExternalMetadataService : IExternalMetadataService
{
// Find the series based on name and type and that the user has access too
var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames,
series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId),
libraryType, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId),
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId));
if (seriesForRec != null)

View file

@ -54,7 +54,6 @@ public class SeriesService : ISeriesService
private readonly ILogger<SeriesService> _logger;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
private readonly IEasyCachingProvider _cacheProvider;
private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto
{
@ -64,8 +63,7 @@ public class SeriesService : ISeriesService
};
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService,
IEasyCachingProviderFactory cachingProviderFactory)
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
@ -73,9 +71,6 @@ public class SeriesService : ISeriesService
_logger = logger;
_scrobblingService = scrobblingService;
_localizationService = localizationService;
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusSeriesDetail);
}
/// <summary>
@ -114,7 +109,6 @@ public class SeriesService : ISeriesService
/// <returns></returns>
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{
var hasWebLinksChanged = false;
try
{
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
@ -170,8 +164,6 @@ public class SeriesService : ISeriesService
series.Metadata.WebLinks = string.Empty;
} else
{
hasWebLinksChanged =
series.Metadata.WebLinks != updateSeriesMetadataDto.SeriesMetadata?.WebLinks;
series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks
.Split(",")
.Where(s => !string.IsNullOrEmpty(s))
@ -314,13 +306,6 @@ public class SeriesService : ISeriesService
_logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work");
}
if (hasWebLinksChanged)
{
_logger.LogDebug("Clearing cache as series weblinks may have changed");
await _cacheProvider.RemoveAsync(MetadataController.CacheKey + seriesId);
}
if (updateSeriesMetadataDto.CollectionTags == null) return true;
foreach (var tag in updateSeriesMetadataDto.CollectionTags)
{