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? reviews) { if (reviews == null) return; context.ExternalReview.RemoveRange(reviews); } public void Remove(IEnumerable? ratings) { if (ratings == null) return; context.ExternalRating.RemoveRange(ratings); } public void Remove(IEnumerable? recommendations) { if (recommendations == null) return; context.ExternalRecommendation.RemoveRange(recommendations); } public void Remove(ExternalSeriesMetadata? metadata) { if (metadata == null) return; context.ExternalSeriesMetadata.Remove(metadata); } /// /// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables /// /// /// /// public Task 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 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 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) .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(mapper.ConfigurationProvider) .ToListAsync(ct); IEnumerable reviews = []; if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) { reviews = seriesDetailDto.ExternalReviews .Select(r => { var ret = mapper.Map(r); ret.IsExternal = true; return ret; }) .OrderByDescending(r => r.Score); } IEnumerable ratings = []; if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0) { ratings = seriesDetailDto.ExternalRatings .Select(mapper.Map); } var seriesDetailPlusDto = new SeriesDetailPlusDto() { Ratings = ratings, Reviews = reviews, Recommendations = new RecommendationDto() { ExternalSeries = externalSeriesRecommendations, OwnedSeries = ownedSeriesRecommendations } }; return seriesDetailPlusDto; } /// /// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name /// /// /// /// 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 IsBlacklistedSeries(int seriesId, CancellationToken ct = default) { return context.Series .Where(s => s.Id == seriesId) .Select(s => s.IsBlacklisted) .FirstOrDefaultAsync(ct); } public async Task> 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> 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(mapper.ConfigurationProvider); return PagedList.CreateAsync(source, userParams, ct); } }