Kavita+ Enhancements (#2616)

This commit is contained in:
Joe Milazzo 2024-01-17 17:45:39 -06:00 committed by GitHub
parent 625c56b265
commit dd44f55747
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1056 additions and 468 deletions

View file

@ -233,7 +233,7 @@ public class DownloadController : BaseApiController
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}", 1F));
return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(filename), true);
return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(filename), true);
}
}

View file

@ -2,7 +2,6 @@
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.DTOs.Account;
using API.DTOs.License;
using API.Entities.Enums;
using API.Extensions;
@ -20,7 +19,8 @@ public class LicenseController(
IUnitOfWork unitOfWork,
ILogger<LicenseController> logger,
ILicenseService licenseService,
ILocalizationService localizationService)
ILocalizationService localizationService,
ITaskScheduler taskScheduler)
: BaseApiController
{
/// <summary>
@ -31,7 +31,12 @@ public class LicenseController(
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
{
return Ok(await licenseService.HasActiveLicense(forceCheck));
var ret = await licenseService.HasActiveLicense(forceCheck);
if (ret)
{
await taskScheduler.ScheduleKavitaPlusTasks();
}
return Ok(ret);
}
/// <summary>
@ -57,6 +62,7 @@ public class LicenseController(
setting.Value = null;
unitOfWork.SettingsRepository.Update(setting);
await unitOfWork.CommitAsync();
await taskScheduler.ScheduleKavitaPlusTasks();
return Ok();
}
@ -82,6 +88,7 @@ public class LicenseController(
try
{
await licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim(), dto.DiscordId);
await taskScheduler.ScheduleKavitaPlusTasks();
}
catch (Exception ex)
{

View file

@ -8,9 +8,11 @@ using API.Data;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.DTOs.SeriesDetail;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Plus;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Mvc;
@ -18,17 +20,10 @@ namespace API.Controllers;
#nullable enable
public class MetadataController : BaseApiController
public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService, ILicenseService licenseService,
IRatingService ratingService, IReviewService reviewService, IRecommendationService recommendationService, IExternalMetadataService metadataService)
: BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILocalizationService _localizationService;
public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_localizationService = localizationService;
}
/// <summary>
/// Fetches genres from the instance
/// </summary>
@ -41,10 +36,10 @@ public class MetadataController : BaseApiController
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId()));
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId()));
}
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId()));
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId()));
}
/// <summary>
@ -57,8 +52,8 @@ public class MetadataController : BaseApiController
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.GetAllPersonDtosAsync(User.GetUserId()));
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) :
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
}
/// <summary>
@ -73,9 +68,9 @@ public class MetadataController : BaseApiController
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId()));
return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId()));
}
return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
return Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
}
/// <summary>
@ -90,9 +85,9 @@ public class MetadataController : BaseApiController
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId()));
return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId()));
}
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId()));
return Ok(await unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId()));
}
/// <summary>
@ -108,7 +103,7 @@ public class MetadataController : BaseApiController
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
return Ok(await unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
}
return Ok(Enum.GetValues<AgeRating>().Select(t => new AgeRatingDto()
@ -131,7 +126,7 @@ public class MetadataController : BaseApiController
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids is {Count: > 0})
{
return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
return Ok(unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
}
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()
@ -152,10 +147,13 @@ public class MetadataController : BaseApiController
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
return Ok(await unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
}
/// <summary>
/// Returns all languages Kavita can accept
/// </summary>
/// <returns></returns>
[HttpGet("all-languages")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
public IEnumerable<LanguageDto> GetAllValidLanguages()
@ -177,9 +175,38 @@ public class MetadataController : BaseApiController
[HttpGet("chapter-summary")]
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
{
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"));
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>
/// Fetches the details needed from Kavita+ for Series Detail page
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("series-detail-plus")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId)
{
var seriesDetail = new SeriesDetailPlusDto();
if (!await licenseService.HasActiveLicense())
{
seriesDetail.Recommendations = null;
seriesDetail.Ratings = Enumerable.Empty<RatingDto>();
return Ok(seriesDetail);
}
seriesDetail = await metadataService.GetSeriesDetail(User.GetUserId(), seriesId);
// Temp solution, needs to be updated with new API
// seriesDetail.Ratings = await ratingService.GetRatings(seriesId);
// seriesDetail.Reviews = await reviewService.GetReviewsForSeries(User.GetUserId(), seriesId);
// seriesDetail.Recommendations =
// await recommendationService.GetRecommendationsForSeries(User.GetUserId(), seriesId);
return Ok(seriesDetail);
}
}

View file

@ -44,26 +44,14 @@ public class RatingController : BaseApiController
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<IEnumerable<RatingDto>>> GetRating(int seriesId)
{
if (!await _licenseService.HasActiveLicense())
{
return Ok(Enumerable.Empty<RatingDto>());
}
var cacheKey = CacheKey + seriesId;
var results = await _cacheProvider.GetAsync<IEnumerable<RatingDto>>(cacheKey);
if (results.HasValue)
{
return Ok(results.Value);
}
var ratings = await _ratingService.GetRatings(seriesId);
await _cacheProvider.SetAsync(cacheKey, ratings, TimeSpan.FromHours(24));
_logger.LogDebug("Caching external rating for {Key}", cacheKey);
return Ok(ratings);
return Ok(await _ratingService.GetRatings(seriesId));
}
[HttpGet("overall")]

View file

@ -51,78 +51,10 @@ public class ReviewController : BaseApiController
/// </summary>
/// <param name="seriesId"></param>
[HttpGet]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<IEnumerable<UserReviewDto>>> GetReviews(int seriesId)
{
var userId = User.GetUserId();
var username = User.GetUsername();
var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId))
.Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(username) ? 1 : 0)
.ToList();
if (!await _licenseService.HasActiveLicense())
{
return Ok(userRatings);
}
var cacheKey = CacheKey + seriesId;
IList<UserReviewDto> externalReviews;
var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey);
if (result.HasValue)
{
externalReviews = result.Value.ToList();
}
else
{
var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList();
externalReviews = SelectSpectrumOfReviews(reviews);
await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10));
_logger.LogDebug("Caching external reviews for {Key}", cacheKey);
}
// Fetch external reviews and splice them in
userRatings.AddRange(externalReviews);
return Ok(userRatings);
}
private static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
{
IList<UserReviewDto> externalReviews;
var totalReviews = reviews.Count;
if (totalReviews > 10)
{
var stepSize = Math.Max((totalReviews - 4) / 8, 1);
var selectedReviews = new List<UserReviewDto>()
{
reviews[0],
reviews[1],
};
for (var i = 2; i < totalReviews - 2; i += stepSize)
{
selectedReviews.Add(reviews[i]);
if (selectedReviews.Count >= 8)
break;
}
selectedReviews.Add(reviews[totalReviews - 2]);
selectedReviews.Add(reviews[totalReviews - 1]);
externalReviews = selectedReviews;
}
else
{
externalReviews = reviews;
}
return externalReviews;
return Ok(await _reviewService.GetReviewsForSeries(User.GetUserId(), seriesId));
}
/// <summary>