Skip to content

Commit

Permalink
Customized Scheduler + Saved Kavita+ Details (Kareadita#2644)
Browse files Browse the repository at this point in the history
  • Loading branch information
majora2007 authored Jan 22, 2024
1 parent 2092e12 commit ad74871
Show file tree
Hide file tree
Showing 76 changed files with 6,267 additions and 3,561 deletions.
3 changes: 2 additions & 1 deletion API.Tests/Converters/CronConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public class CronConverterTests
[InlineData("disabled", "0 0 31 2 *")]
[InlineData("weekly", "0 0 * * 1")]
[InlineData("", "0 0 31 2 *")]
[InlineData("sdfgdf", "")]
[InlineData("sdfgdf", "sdfgdf")]
[InlineData("* * * * *", "* * * * *")]
public void ConvertTest(string input, string expected)
{
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
Expand Down
4 changes: 3 additions & 1 deletion API.Tests/Services/SeriesServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using API.Services.Plus;
using API.SignalR;
using API.Tests.Helpers;
using EasyCaching.Core;
using Hangfire;
using Hangfire.InMemory;
using Microsoft.Extensions.Caching.Memory;
Expand Down Expand Up @@ -58,7 +59,8 @@ public SeriesServiceTests() : base()

_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
Substitute.For<IScrobblingService>(), locService);
Substitute.For<IScrobblingService>(), locService,
Substitute.For<IEasyCachingProviderFactory>());
}
#region Setup

Expand Down
2 changes: 1 addition & 1 deletion API/API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
<PackageReference Include="NetVips.Native" Version="8.15.1" />
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
Expand Down
9 changes: 3 additions & 6 deletions API/Constants/CacheProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ public static class EasyCacheProfiles
/// Cache the libraries on the server
/// </summary>
public const string Library = "library";
public const string KavitaPlusExternalSeries = "kavita+externalSeries";
/// <summary>
/// Metadata filter
/// Series Detail page for Kavita+ stuff
/// </summary>
public const string Filter = "filter";
public const string KavitaPlusReviews = "kavita+reviews";
public const string KavitaPlusRecommendations = "kavita+recommendations";
public const string KavitaPlusRatings = "kavita+ratings";
public const string KavitaPlusExternalSeries = "kavita+externalSeries";
public const string KavitaPlusSeriesDetail = "kavita+seriesDetail";
}
42 changes: 36 additions & 6 deletions API/Controllers/MetadataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using API.Extensions;
using API.Services;
using API.Services.Plus;
using EasyCaching.Core;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -21,9 +22,12 @@ namespace API.Controllers;
#nullable enable

public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService, ILicenseService licenseService,
IRatingService ratingService, IReviewService reviewService, IRecommendationService recommendationService, IExternalMetadataService metadataService)
IExternalMetadataService metadataService, IEasyCachingProviderFactory cachingProviderFactory)
: BaseApiController
{
private readonly IEasyCachingProvider _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusSeriesDetail);
public const string CacheKey = "kavitaPlusSeriesDetail_";

/// <summary>
/// Fetches genres from the instance
/// </summary>
Expand All @@ -48,7 +52,7 @@ public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? library
/// <param name="role">role</param>
/// <returns></returns>
[HttpGet("people-by-role")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["role"])]
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(PersonRole? role)
{
return role.HasValue ?
Expand All @@ -62,7 +66,7 @@ public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(PersonRole? role)
/// <param name="libraryIds">String separated libraryIds or null for all people</param>
/// <returns></returns>
[HttpGet("people")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])]
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
Expand All @@ -79,7 +83,7 @@ public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryId
/// <param name="libraryIds">String separated libraryIds or null for all tags</param>
/// <returns></returns>
[HttpGet("tags")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])]
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
{
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
Expand All @@ -96,7 +100,7 @@ public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
/// <returns></returns>
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])]
[HttpGet("age-ratings")]
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
{
Expand Down Expand Up @@ -184,6 +188,7 @@ public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
/// <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>
/// <returns></returns>
[HttpGet("series-detail-plus")]
Expand All @@ -195,7 +200,32 @@ public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailDa
return Ok(null);
}

return Ok(await metadataService.GetSeriesDetail(User.GetUserId(), seriesId));
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
if (user == null) return Unauthorized();
var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, user.Id))
.Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
.ToList();

var cacheKey = CacheKey + seriesId + "_" + user.Id;
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey);
if (results.HasValue)
{
var cachedResult = results.Value;
userReviews.AddRange(cachedResult.Reviews);
cachedResult.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
return cachedResult;
}

var ret = await metadataService.GetSeriesDetail(user.Id, seriesId);
if (ret == null) return Ok(null);
userReviews.AddRange(ret.Reviews);
ret.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);


await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(24));

return Ok(ret);

}
}
28 changes: 1 addition & 27 deletions API/Controllers/RatingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,12 @@ namespace API.Controllers;
/// </summary>
public class RatingController : BaseApiController
{
private readonly ILicenseService _licenseService;
private readonly IRatingService _ratingService;
private readonly ILogger<RatingController> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly IEasyCachingProvider _cacheProvider;
public const string CacheKey = "rating_";

public RatingController(ILicenseService licenseService, IRatingService ratingService,
ILogger<RatingController> logger, IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork)
public RatingController(IUnitOfWork unitOfWork)
{
_licenseService = licenseService;
_ratingService = ratingService;
_logger = logger;
_unitOfWork = unitOfWork;

_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
}

/// <summary>
/// Get the external ratings for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<IEnumerable<RatingDto>>> GetRating(int seriesId)
{
if (!await _licenseService.HasActiveLicense())
{
return Ok(Enumerable.Empty<RatingDto>());
}
return Ok(await _ratingService.GetRatings(seriesId));
}

[HttpGet("overall")]
Expand Down
68 changes: 8 additions & 60 deletions API/Controllers/RecommendedController.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.DTOs.Recommendation;
using API.Extensions;
using API.Helpers;
using API.Services;
using API.Services.Plus;
using EasyCaching.Core;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;

namespace API.Controllers;

Expand All @@ -22,64 +12,22 @@ namespace API.Controllers;
public class RecommendedController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRecommendationService _recommendationService;
private readonly ILicenseService _licenseService;
private readonly ILocalizationService _localizationService;
private readonly IEasyCachingProvider _cacheProvider;

public const string CacheKey = "recommendation_";

public RecommendedController(IUnitOfWork unitOfWork, IRecommendationService recommendationService,
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory,
ILocalizationService localizationService)
public RecommendedController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_recommendationService = recommendationService;
_licenseService = licenseService;
_localizationService = localizationService;
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
}

/// <summary>
/// For Kavita+ users, this will return recommendations on the server.
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("recommendations")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})]
public async Task<ActionResult<RecommendationDto>> GetRecommendations(int seriesId)
{
var userId = User.GetUserId();
if (!await _licenseService.HasActiveLicense())
{
return Ok(new RecommendationDto());
}

if (!await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-restricted"));
}

var cacheKey = $"{CacheKey}-{seriesId}-{userId}";
var results = await _cacheProvider.GetAsync<RecommendationDto>(cacheKey);
if (results.HasValue)
{
return Ok(results.Value);
}

var ret = await _recommendationService.GetRecommendationsForSeries(userId, seriesId);
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(10));
return Ok(ret);
}


/// <summary>
/// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release.
/// </summary>
/// <param name="libraryId">Library to restrict series to</param>
/// <param name="userParams">Pagination</param>
/// <returns></returns>
[HttpGet("quick-reads")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams);
Expand All @@ -95,7 +43,7 @@ public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryI
/// <param name="userParams"></param>
/// <returns></returns>
[HttpGet("quick-catchup-reads")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams)
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams);
Expand All @@ -111,7 +59,7 @@ public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int l
/// <param name="userParams">Pagination</param>
/// <returns></returns>
[HttpGet("highly-rated")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams? userParams)
{
var userId = User.GetUserId();
userParams ??= UserParams.Default;
Expand All @@ -129,7 +77,7 @@ public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int library
/// <param name="userParams">Pagination</param>
/// <returns></returns>
[HttpGet("more-in")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams? userParams)
{
var userId = User.GetUserId();

Expand All @@ -148,7 +96,7 @@ public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, i
/// <param name="userParams">Pagination</param>
/// <returns></returns>
[HttpGet("rediscover")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams);
Expand Down
34 changes: 3 additions & 31 deletions API/Controllers/ReviewController.cs
Original file line number Diff line number Diff line change
@@ -1,62 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs.SeriesDetail;
using API.Extensions;
using API.Helpers.Builders;
using API.Services;
using API.Services.Plus;
using AutoMapper;
using EasyCaching.Core;
using Hangfire;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace API.Controllers;

#nullable enable

public class ReviewController : BaseApiController
{
private readonly ILogger<ReviewController> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ILicenseService _licenseService;
private readonly IMapper _mapper;
private readonly IReviewService _reviewService;
private readonly IScrobblingService _scrobblingService;
private readonly IEasyCachingProvider _cacheProvider;
public const string CacheKey = "review_";

public ReviewController(ILogger<ReviewController> logger, IUnitOfWork unitOfWork, ILicenseService licenseService,
IMapper mapper, IReviewService reviewService, IScrobblingService scrobblingService,
IEasyCachingProviderFactory cachingProviderFactory)
public ReviewController(IUnitOfWork unitOfWork,
IMapper mapper, IScrobblingService scrobblingService)
{
_logger = logger;
_unitOfWork = unitOfWork;
_licenseService = licenseService;
_mapper = mapper;
_reviewService = reviewService;
_scrobblingService = scrobblingService;

_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
}


/// <summary>
/// Fetches reviews from the server for a given series
/// </summary>
/// <param name="seriesId"></param>
[HttpGet]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])]
public async Task<ActionResult<IEnumerable<UserReviewDto>>> GetReviews(int seriesId)
{
return Ok(await _reviewService.GetReviewsForSeries(User.GetUserId(), seriesId));
}

/// <summary>
/// Updates the review for a given series
/// </summary>
Expand Down
Loading

0 comments on commit ad74871

Please sign in to comment.