Kavita/Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs
Fesaa c62b20f54b
BE Tech Debt (#4497)
Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
Co-authored-by: Joe Milazzo <josephmajora@gmail.com>
2026-03-07 10:04:08 -08:00

219 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Kavita.API.Repositories;
using Kavita.API.Services;
using Kavita.API.Services.Plus;
using Kavita.Common.Helpers;
using Kavita.Database.Extensions;
using Kavita.Models.DTOs;
using Kavita.Models.DTOs.KavitaPlus.Manage;
using Kavita.Models.DTOs.Recommendation;
using Kavita.Models.DTOs.SeriesDetail;
using Kavita.Models.Entities;
using Kavita.Models.Entities.Enums;
using Kavita.Models.Entities.Metadata;
using Microsoft.EntityFrameworkCore;
namespace Kavita.Database.Repositories;
public class ExternalSeriesMetadataRepository(DataContext context, IMapper mapper) : IExternalSeriesMetadataRepository
{
public void Attach(ExternalSeriesMetadata metadata)
{
context.ExternalSeriesMetadata.Attach(metadata);
}
public void Attach(ExternalRating rating)
{
context.ExternalRating.Attach(rating);
}
public void Attach(ExternalReview review)
{
context.ExternalReview.Attach(review);
}
public void Remove(IEnumerable<ExternalReview>? reviews)
{
if (reviews == null) return;
context.ExternalReview.RemoveRange(reviews);
}
public void Remove(IEnumerable<ExternalRating>? ratings)
{
if (ratings == null) return;
context.ExternalRating.RemoveRange(ratings);
}
public void Remove(IEnumerable<ExternalRecommendation>? recommendations)
{
if (recommendations == null) return;
context.ExternalRecommendation.RemoveRange(recommendations);
}
public void Remove(ExternalSeriesMetadata? metadata)
{
if (metadata == null) return;
context.ExternalSeriesMetadata.Remove(metadata);
}
/// <summary>
/// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables
/// </summary>
/// <param name="seriesId"></param>
/// <param name="ct"></param>
/// <returns></returns>
public Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default)
{
return context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.Include(s => s.ExternalReviews)
.Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore))
.Include(s => s.ExternalRecommendations.OrderBy(r => r.Id))
.AsSplitQuery()
.FirstOrDefaultAsync(ct);
}
public async Task<bool> NeedsDataRefresh(int seriesId, CancellationToken ct = default)
{
return await context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.Select(s => s.ValidUntilUtc)
.Where(date => date < DateTime.UtcNow)
.AnyAsync(ct);
}
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId, CancellationToken ct = default)
{
var seriesDetailDto = await context.ExternalSeriesMetadata
.Where(m => m.SeriesId == seriesId)
.Include(m => m.ExternalRatings)
.Include(m => m.ExternalReviews)
.Include(m => m.ExternalRecommendations)
.FirstOrDefaultAsync(ct);
if (seriesDetailDto == null)
{
return null; // or handle the case when seriesDetailDto is not found
}
var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations
.Where(r => r.SeriesId == null)
.Select(mapper.Map<ExternalSeriesDto>)
.ToList();
var ownedIds = seriesDetailDto.ExternalRecommendations
.Where(r => r.SeriesId != null)
.Select(r => r.SeriesId)
.ToList();
var ownedSeriesRecommendations = await context.Series
.Where(s => ownedIds.Contains(s.Id))
.OrderBy(s => s.SortName.ToLower())
.ProjectTo<SeriesDto>(mapper.ConfigurationProvider)
.ToListAsync(ct);
IEnumerable<UserReviewDto> reviews = [];
if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any())
{
reviews = seriesDetailDto.ExternalReviews
.Select(r =>
{
var ret = mapper.Map<UserReviewDto>(r);
ret.IsExternal = true;
return ret;
})
.OrderByDescending(r => r.Score);
}
IEnumerable<RatingDto> ratings = [];
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0)
{
ratings = seriesDetailDto.ExternalRatings
.Select(mapper.Map<RatingDto>);
}
var seriesDetailPlusDto = new SeriesDetailPlusDto()
{
Ratings = ratings,
Reviews = reviews,
Recommendations = new RecommendationDto()
{
ExternalSeries = externalSeriesRecommendations,
OwnedSeries = ownedSeriesRecommendations
}
};
return seriesDetailPlusDto;
}
/// <summary>
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
/// </summary>
/// <param name="series"></param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task LinkRecommendationsToSeries(Series series, CancellationToken ct = default)
{
var recMatches = await context.ExternalRecommendation
.Where(r => r.SeriesId == null || r.SeriesId == 0)
.Where(r => EF.Functions.Like(r.Name, series.Name) ||
EF.Functions.Like(r.Name, series.LocalizedName))
.ToListAsync(ct);
foreach (var rec in recMatches)
{
rec.SeriesId = series.Id;
}
await context.SaveChangesAsync(ct);
}
public Task<bool> IsBlacklistedSeries(int seriesId, CancellationToken ct = default)
{
return context.Series
.Where(s => s.Id == seriesId)
.Select(s => s.IsBlacklisted)
.FirstOrDefaultAsync(ct);
}
public async Task<IList<int>> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false,
CancellationToken ct = default)
{
return await context.Series
.Where(s => !IExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.Where(s => s.Library.AllowMetadataMatching)
.WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0)
.Where(s => !s.IsBlacklisted && !s.DontMatch)
.OrderByDescending(s => s.Library.Type)
.ThenBy(s => s.NormalizedName)
.Select(s => s.Id)
.Take(limit)
.ToListAsync(ct);
}
public Task<PagedList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams,
CancellationToken ct = default)
{
var source = context.Series
.Include(s => s.Library)
.Include(s => s.ExternalSeriesMetadata)
.Where(s => !IExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.Where(s => s.Library.AllowMetadataMatching)
.WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType)
.FilterMatchState(filter.MatchStateOption)
.OrderBy(s => s.NormalizedName)
.ProjectTo<ManageMatchSeriesDto>(mapper.ConfigurationProvider);
return PagedList<ManageMatchSeriesDto>.CreateAsync(source, userParams, ct);
}
}