More Polish (#2665)

This commit is contained in:
Joe Milazzo 2024-01-29 13:44:20 -06:00 committed by GitHub
parent 47547c23dd
commit e8d9a8b3a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 151 additions and 91 deletions

View File

@ -703,6 +703,37 @@ public class ReaderServiceTests
Assert.Equal(-1, nextChapter); Assert.Equal(-1, nextChapter);
} }
// This is commented out because, while valid, I can't solve how to make this pass (https://github.com/Kareadita/Kavita/issues/2099)
// [Fact]
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume()
// {
// await ResetDb();
//
// var series = new SeriesBuilder("Test")
// .WithVolume(new VolumeBuilder("0")
// .WithMinNumber(0)
// .WithChapter(new ChapterBuilder("1").Build())
// .WithChapter(new ChapterBuilder("2").Build())
// .Build())
// .WithVolume(new VolumeBuilder("1")
// .WithMinNumber(1)
// .WithChapter(new ChapterBuilder("0").Build())
// .Build())
// .Build();
// series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
//
// _context.Series.Add(series);
// _context.AppUser.Add(new AppUser()
// {
// UserName = "majora2007"
// });
//
// await _context.SaveChangesAsync();
//
// var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
// Assert.Equal(-1, nextChapter);
// }
// This is commented out because, while valid, I can't solve how to make this pass // This is commented out because, while valid, I can't solve how to make this pass
// [Fact] // [Fact]
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials() // public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials()

View File

@ -5,11 +5,14 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Data; using API.Data;
using API.Data.Misc;
using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.DTOs.Filtering; using API.DTOs.Filtering;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Recommendation; using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Services; using API.Services;
@ -193,7 +196,6 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("series-detail-plus")] [HttpGet("series-detail-plus")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId) public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId)
{ {
if (!await licenseService.HasActiveLicense()) if (!await licenseService.HasActiveLicense())
@ -203,6 +205,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, user.Id)) var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, user.Id))
.Where(r => !string.IsNullOrEmpty(r.Body)) .Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0) .OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
@ -213,28 +216,35 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
if (results.HasValue) if (results.HasValue)
{ {
var cachedResult = results.Value; var cachedResult = results.Value;
userReviews.AddRange(cachedResult.Reviews); await PrepareSeriesDetail(userReviews, cachedResult, user);
cachedResult.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
if (!await unitOfWork.UserRepository.IsUserAdminAsync(user))
{
cachedResult.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
}
return cachedResult; return cachedResult;
} }
var ret = await metadataService.GetSeriesDetail(user.Id, seriesId); var ret = await metadataService.GetSeriesDetail(user.Id, seriesId);
if (ret == null) return Ok(null); if (ret == null) return Ok(null);
userReviews.AddRange(ret.Reviews); await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48));
ret.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(24));
if (!await unitOfWork.UserRepository.IsUserAdminAsync(user)) // For some reason if we don't use a different instance, the cache keeps changes made below
{ var newCacheResult = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey)).Value;
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>(); await PrepareSeriesDetail(userReviews, newCacheResult, user);
}
return Ok(ret); return Ok(newCacheResult);
} }
private async Task PrepareSeriesDetail(List<UserReviewDto> userReviews, SeriesDetailPlusDto ret, AppUser user)
{
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList()));
ret.Reviews = userReviews;
if (!isAdmin)
{
// Re-obtain owned series and take into account age restriction
ret.Recommendations.OwnedSeries =
await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync(
ret.Recommendations.OwnedSeries.Select(s => s.Id), user);
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
}
}
} }

View File

@ -1250,7 +1250,7 @@ public class OpdsController : BaseApiController
if (progress != null) if (progress != null)
{ {
link.LastRead = progress.PageNum; link.LastRead = progress.PageNum;
link.LastReadDate = progress.LastModifiedUtc; link.LastReadDate = progress.LastModifiedUtc.ToString("o"); // Adhere to ISO 8601
} }
link.IsPageStream = true; link.IsPageStream = true;
return link; return link;

View File

@ -57,7 +57,7 @@ public class PanelsController : BaseApiController
PageNum = 0, PageNum = 0,
ChapterId = chapterId, ChapterId = chapterId,
VolumeId = 0, VolumeId = 0,
SeriesId = 0 SeriesId = 0,
}); });
return Ok(progress); return Ok(progress);
} }

View File

@ -39,7 +39,7 @@ public class FeedLink
/// </summary> /// </summary>
/// <remarks>Attribute MUST conform Atom's Date construct</remarks> /// <remarks>Attribute MUST conform Atom's Date construct</remarks>
[XmlAttribute("lastReadDate", Namespace = "http://vaemendis.net/opds-pse/ns")] [XmlAttribute("lastReadDate", Namespace = "http://vaemendis.net/opds-pse/ns")]
public DateTime LastReadDate { get; set; } public string LastReadDate { get; set; }
public bool ShouldSerializeLastReadDate() public bool ShouldSerializeLastReadDate()
{ {

View File

@ -15,6 +15,11 @@ public class VolumeDto : IHasReadTimeEstimate
public float MaxNumber { get; set; } public float MaxNumber { get; set; }
/// <inheritdoc cref="Volume.Name"/> /// <inheritdoc cref="Volume.Name"/>
public string Name { get; set; } = default!; public string Name { get; set; } = default!;
/// <summary>
/// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14
/// </summary>
[Obsolete("Use MinNumber")]
public float Number { get; set; }
public int Pages { get; set; } public int Pages { get; set; }
public int PagesRead { get; set; } public int PagesRead { get; set; }
public DateTime LastModifiedUtc { get; set; } public DateTime LastModifiedUtc { get; set; }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
@ -26,6 +27,7 @@ public interface IExternalSeriesMetadataRepository
void Remove(IEnumerable<ExternalRating>? ratings); void Remove(IEnumerable<ExternalRating>? ratings);
void Remove(IEnumerable<ExternalRecommendation>? recommendations); void Remove(IEnumerable<ExternalRecommendation>? recommendations);
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId, int limit = 25); Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId, int limit = 25);
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId, DateTime expireTime);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user); Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user);
Task LinkRecommendationsToSeries(Series series); Task LinkRecommendationsToSeries(Series series);
} }
@ -86,24 +88,22 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return _context.ExternalSeriesMetadata return _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId) .Where(s => s.SeriesId == seriesId)
.Include(s => s.ExternalReviews.Take(limit)) .Include(s => s.ExternalReviews.Take(limit))
.Include(s => s.ExternalRatings.Take(limit)) .Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore).Take(limit))
.Include(s => s.ExternalRecommendations.Take(limit)) .Include(s => s.ExternalRecommendations.OrderBy(r => r.Id).Take(limit))
.AsSplitQuery() .AsSplitQuery()
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId, DateTime expireTime)
{
var row = await _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.FirstOrDefaultAsync();
return row == null || row.LastUpdatedUtc <= expireTime;
}
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user) public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user)
{ {
var canSeeExternalSeries = user is { AgeRestriction: AgeRating.NotApplicable } &&
await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
var allowedLibraries = await _context.Library
.Where(library => library.AppUsers.Any(x => x.Id == user.Id))
.Select(l => l.Id)
.ToListAsync();
var userRating = await _context.AppUser.GetUserAgeRestriction(user.Id);
var seriesDetailDto = await _context.ExternalSeriesMetadata var seriesDetailDto = await _context.ExternalSeriesMetadata
.Where(m => m.SeriesId == seriesId) .Where(m => m.SeriesId == seriesId)
.Include(m => m.ExternalRatings) .Include(m => m.ExternalRatings)
@ -116,29 +116,30 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return null; // or handle the case when seriesDetailDto is not found return null; // or handle the case when seriesDetailDto is not found
} }
var externalSeriesRecommendations = new List<ExternalSeriesDto>(); var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations
if (!canSeeExternalSeries) .Where(r => r.SeriesId == null)
{ .Select(r => _mapper.Map<ExternalSeriesDto>(r))
externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations .ToList();
.Where(r => r.SeriesId is null or 0)
.Select(r => _mapper.Map<ExternalSeriesDto>(r))
.ToList();
}
var ownedIds = seriesDetailDto.ExternalRecommendations
.Where(r => r.SeriesId != null)
.Select(r => r.SeriesId)
.ToList();
var ownedSeriesRecommendations = await _context.ExternalRecommendation var ownedSeriesRecommendations = await _context.Series
.Where(r => r.SeriesId > 0 && allowedLibraries.Contains(r.Series.LibraryId)) .Where(s => ownedIds.Contains(s.Id))
.Join(_context.Series, r => r.SeriesId, s => s.Id, (recommendation, series) => series)
.RestrictAgainstAgeRestriction(userRating)
.OrderBy(s => s.SortName.ToLower()) .OrderBy(s => s.SortName.ToLower())
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider) .ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync(); .ToListAsync();
var seriesDetailPlusDto = new SeriesDetailPlusDto() var seriesDetailPlusDto = new SeriesDetailPlusDto()
{ {
Ratings = seriesDetailDto.ExternalRatings.Select(r => _mapper.Map<RatingDto>(r)), Ratings = seriesDetailDto.ExternalRatings
Reviews = seriesDetailDto.ExternalReviews.OrderByDescending(r => r.Score) .DefaultIfEmpty()
.Select(r => _mapper.Map<RatingDto>(r)),
Reviews = seriesDetailDto.ExternalReviews
.DefaultIfEmpty()
.OrderByDescending(r => r.Score)
.Select(r => .Select(r =>
{ {
var ret = _mapper.Map<UserReviewDto>(r); var ret = _mapper.Map<UserReviewDto>(r);

View File

@ -1,11 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Data.ManualMigrations;
using API.Data.Misc; using API.Data.Misc;
using API.Data.Scanner; using API.Data.Scanner;
using API.DTOs; using API.DTOs;
@ -14,7 +12,6 @@ using API.DTOs.Dashboard;
using API.DTOs.Filtering; using API.DTOs.Filtering;
using API.DTOs.Filtering.v2; using API.DTOs.Filtering.v2;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Reader;
using API.DTOs.ReadingLists; using API.DTOs.ReadingLists;
using API.DTOs.Search; using API.DTOs.Search;
using API.DTOs.SeriesDetail; using API.DTOs.SeriesDetail;
@ -34,7 +31,6 @@ using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SQLite;
namespace API.Data.Repositories; namespace API.Data.Repositories;
@ -95,6 +91,7 @@ public interface ISeriesRepository
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None); Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None);
Task<SeriesDto?> GetSeriesDtoByIdAsync(int seriesId, int userId); Task<SeriesDto?> GetSeriesDtoByIdAsync(int seriesId, int userId);
Task<Series?> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata); Task<Series?> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
Task<IList<SeriesDto>> GetSeriesDtoByIdsAsync(IEnumerable<int> seriesIds, AppUser user);
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds); Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds); Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds);
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
@ -569,6 +566,26 @@ public class SeriesRepository : ISeriesRepository
.ToListAsync(); .ToListAsync();
} }
public async Task<IList<SeriesDto>> GetSeriesDtoByIdsAsync(IEnumerable<int> seriesIds, AppUser user)
{
var allowedLibraries = await _context.Library
.Where(library => library.AppUsers.Any(x => x.Id == user.Id))
.Select(l => l.Id)
.ToListAsync();
var restriction = new AgeRestriction()
{
AgeRating = user.AgeRestriction,
IncludeUnknowns = user.AgeRestrictionIncludeUnknowns
};
return await _context.Series
.Include(s => s.Metadata)
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
.RestrictAgainstAgeRestriction(restriction)
.AsSplitQuery()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds) public async Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds)
{ {
var volumes = await _context.Volume var volumes = await _context.Volume
@ -1889,22 +1906,25 @@ public class SeriesRepository : ISeriesRepository
} }
/// <summary> /// <summary>
/// Uses multiple names to find a match against a series then ensures the user has appropriate access to it. If not, returns null. /// Uses multiple names to find a match against a series. If not, returns null.
/// </summary> /// </summary>
/// <remarks>This does not restrict to the user at all. That is handled at the API level.</remarks>
/// <param name="userId"></param> /// <param name="userId"></param>
/// <param name="names"></param> /// <param name="names"></param>
/// <returns></returns> /// <returns></returns>
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl) public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
{ {
var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var libraryIds = await _context.Library
var libraryIds = await _context.Library.GetUserLibrariesByType(userId, libraryType).ToListAsync(); .Where(lib => lib.Type == libraryType)
.Select(l => l.Id)
.ToListAsync();
var normalizedNames = names.Select(n => n.ToNormalized()).ToList(); var normalizedNames = names.Select(n => n.ToNormalized()).ToList();
SeriesDto? result = null; SeriesDto? result = null;
if (!string.IsNullOrEmpty(aniListUrl) || !string.IsNullOrEmpty(malUrl)) if (!string.IsNullOrEmpty(aniListUrl) || !string.IsNullOrEmpty(malUrl))
{ {
// TODO: I can likely work AniList and MalIds from ExternalSeriesMetadata in here // TODO: I can likely work AniList and MalIds from ExternalSeriesMetadata in here
result = await _context.Series result = await _context.Series
.RestrictAgainstAgeRestriction(userRating)
.Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks))
.Where(s => libraryIds.Contains(s.Library.Id)) .Where(s => libraryIds.Contains(s.Library.Id))
.WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl)) .WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl))
@ -1917,7 +1937,6 @@ public class SeriesRepository : ISeriesRepository
if (result != null) return result; if (result != null) return result;
return await _context.Series return await _context.Series
.RestrictAgainstAgeRestriction(userRating)
.Where(s => normalizedNames.Contains(s.NormalizedName) || .Where(s => normalizedNames.Contains(s.NormalizedName) ||
normalizedNames.Contains(s.NormalizedLocalizedName)) normalizedNames.Contains(s.NormalizedLocalizedName))
.Where(s => libraryIds.Contains(s.Library.Id)) .Where(s => libraryIds.Contains(s.Library.Id))

View File

@ -25,7 +25,6 @@ public class ExternalReview
/// Reviewer's username /// Reviewer's username
/// </summary> /// </summary>
public string Username { get; set; } public string Username { get; set; }
/// <summary> /// <summary>
/// An Optional Rating coming from the Review /// An Optional Rating coming from the Review
/// </summary> /// </summary>
@ -36,6 +35,7 @@ public class ExternalReview
public int Score { get; set; } public int Score { get; set; }
public int TotalVotes { get; set; } public int TotalVotes { get; set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
// Relationships // Relationships

View File

@ -46,7 +46,8 @@ public class AutoMapperProfiles : Profile
.ForMember(dest => dest.ChapterId, opt => opt.MapFrom(src => src.Bookmark.ChapterId)) .ForMember(dest => dest.ChapterId, opt => opt.MapFrom(src => src.Bookmark.ChapterId))
.ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series)); .ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series));
CreateMap<LibraryDto, Library>(); CreateMap<LibraryDto, Library>();
CreateMap<Volume, VolumeDto>(); CreateMap<Volume, VolumeDto>()
.ForMember(dest => dest.Number, opt => opt.MapFrom(src => src.MinNumber));
CreateMap<MangaFile, MangaFileDto>(); CreateMap<MangaFile, MangaFileDto>();
CreateMap<Chapter, ChapterDto>(); CreateMap<Chapter, ChapterDto>();
CreateMap<Series, SeriesDto>(); CreateMap<Series, SeriesDto>();

View File

@ -101,14 +101,14 @@ public class ExternalMetadataService : IExternalMetadataService
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return new SeriesDetailPlusDto(); if (user == null) return new SeriesDetailPlusDto();
// Let's try to get SeriesDetailPlusDto from the local DB. var needsRefresh =
var externalSeriesMetadata = await GetExternalSeriesMetadataForSeries(seriesId, series); await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId,
var needsRefresh = externalSeriesMetadata.LastUpdatedUtc <= DateTime.UtcNow.Subtract(_externalSeriesMetadataCache); DateTime.UtcNow.Subtract(_externalSeriesMetadataCache));
if (!needsRefresh) if (!needsRefresh)
{ {
// Convert into DTOs and return // Convert into DTOs and return
return await SerializeExternalSeriesDetail(seriesId, series.LibraryId, user); return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, series.LibraryId, user);
} }
try try
@ -127,6 +127,7 @@ public class ExternalMetadataService : IExternalMetadataService
// Clear out existing results // Clear out existing results
var externalSeriesMetadata = await GetExternalSeriesMetadataForSeries(seriesId, series);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews); _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings); _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations); _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations);
@ -150,10 +151,13 @@ public class ExternalMetadataService : IExternalMetadataService
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>(); externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
var recs = await ProcessRecommendations(series, user, result.Recommendations, externalSeriesMetadata); var recs = await ProcessRecommendations(series, user, result.Recommendations, externalSeriesMetadata);
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow; var extRatings = externalSeriesMetadata.ExternalRatings
externalSeriesMetadata.AverageExternalRating = (int) externalSeriesMetadata.ExternalRatings
.Where(r => r.AverageScore > 0) .Where(r => r.AverageScore > 0)
.Average(r => r.AverageScore); .ToList();
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow;
externalSeriesMetadata.AverageExternalRating = extRatings.Count != 0 ? (int) extRatings
.Average(r => r.AverageScore) : 0;
if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value; if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value;
if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value; if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value;
@ -164,11 +168,7 @@ public class ExternalMetadataService : IExternalMetadataService
{ {
Recommendations = recs, Recommendations = recs,
Ratings = result.Ratings, Ratings = result.Ratings,
Reviews = result.Reviews.Select(r => Reviews = externalSeriesMetadata.ExternalReviews.Select(r => _mapper.Map<UserReviewDto>(r))
{
r.IsExternal = true;
return r;
})
}; };
} }
catch (FlurlHttpException ex) catch (FlurlHttpException ex)
@ -186,10 +186,6 @@ public class ExternalMetadataService : IExternalMetadataService
return null; return null;
} }
private async Task<SeriesDetailPlusDto?> SerializeExternalSeriesDetail(int seriesId, int libraryId, AppUser user)
{
return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, libraryId, user);
}
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series) private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
{ {

View File

@ -127,13 +127,14 @@ public class ProcessSeries : IProcessSeries
seriesCollisions = seriesCollisions.Where(collision => seriesCollisions = seriesCollisions.Where(collision =>
collision.Name != firstInfo.Series || collision.LocalizedName != firstInfo.LocalizedSeries).ToList(); collision.Name != firstInfo.Series || collision.LocalizedName != firstInfo.LocalizedSeries).ToList();
if (seriesCollisions.Any()) if (seriesCollisions.Count > 1)
{ {
var tableRows = seriesCollisions.Select(collision => var firstCollision = seriesCollisions[0];
$"<tr><td>Name: {firstInfo.Series}</td><td>Name: {collision.Name}</td></tr>" + var secondCollision = seriesCollisions[1];
$"<tr><td>Localized: {firstInfo.LocalizedSeries}</td><td>Localized: {collision.LocalizedName}</td></tr>" +
$"<tr><td>Filename: {Parser.Parser.NormalizePath(_directoryService.FileSystem.FileInfo.New(firstInfo.FullFilePath).Directory?.ToString())}</td><td>Filename: {Parser.Parser.NormalizePath(collision.FolderPath)}</td></tr>" var tableRows = $"<tr><td>Name: {firstCollision.Name}</td><td>Name: {secondCollision.Name}</td></tr>" +
); $"<tr><td>Localized: {firstCollision.LocalizedName}</td><td>Localized: {secondCollision.LocalizedName}</td></tr>" +
$"<tr><td>Filename: {Parser.Parser.NormalizePath(firstCollision.FolderPath)}</td><td>Filename: {Parser.Parser.NormalizePath(secondCollision.FolderPath)}</td></tr>";
var htmlTable = $"<table class='table table-striped'><thead><tr><th>Series 1</th><th>Series 2</th></tr></thead><tbody>{string.Join(string.Empty, tableRows)}</tbody></table>"; var htmlTable = $"<table class='table table-striped'><thead><tr><th>Series 1</th><th>Series 2</th></tr></thead><tbody>{string.Join(string.Empty, tableRows)}</tbody></table>";

View File

@ -704,22 +704,12 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.ratings = [...data.ratings]; this.ratings = [...data.ratings];
// Recommendations // Recommendations
data.recommendations.ownedSeries.map(r => {
this.seriesService.getMetadata(r.id).subscribe(m => r.summary = m.summary);
});
this.combinedRecs = [...data.recommendations.ownedSeries, ...data.recommendations.externalSeries]; this.combinedRecs = [...data.recommendations.ownedSeries, ...data.recommendations.externalSeries];
this.hasRecommendations = this.combinedRecs.length > 0; this.hasRecommendations = this.combinedRecs.length > 0;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
loadReviews() {
this.seriesService.getReviews(this.seriesId).subscribe(reviews => {
this.reviews = [...reviews];
this.cdRef.markForCheck();
});
}
setContinuePoint() { setContinuePoint() {
this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => { this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.13.9" "version": "0.7.13.10"
}, },
"servers": [ "servers": [
{ {
@ -20337,6 +20337,12 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"number": {
"type": "number",
"description": "This will map to MinNumber. Number was removed in v0.7.13.8",
"format": "float",
"deprecated": true
},
"pages": { "pages": {
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"