using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Constants; 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; #nullable enable 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) { _unitOfWork = unitOfWork; _recommendationService = recommendationService; _licenseService = licenseService; _localizationService = localizationService; _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); } /// /// For Kavita+ users, this will return recommendations on the server. /// /// /// [HttpGet("recommendations")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] public async Task> 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(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); } /// /// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release. /// /// Library to restrict series to /// Pagination /// [HttpGet("quick-reads")] public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } /// /// Quick Catchup Reads are series that should be readable in less than 10 in total and are Ongoing in release. /// /// Library to restrict series to /// /// [HttpGet("quick-catchup-reads")] public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } /// /// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users. /// /// Library to restrict series to /// Pagination /// [HttpGet("highly-rated")] public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams) { var userId = User.GetUserId(); userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } /// /// Chooses a random genre and shows series that are in that without reading progress /// /// Library to restrict series to /// Genre Id /// Pagination /// [HttpGet("more-in")] public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams) { var userId = User.GetUserId(); userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams); await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } /// /// Series that are fully read by the user in no particular order /// /// Library to restrict series to /// Pagination /// [HttpGet("rediscover")] public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } }