Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kavita+ Enhancements #2616

Merged
merged 17 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ad5d26a
Updated dashboard to use control flow syntax
majora2007 Jan 16, 2024
376e9b4
Fixed more in genre not using a localized string for title.
majora2007 Jan 16, 2024
5a92a18
When a series updates to completed and there was a next estimated cha…
majora2007 Jan 16, 2024
1e4159a
Migrated series download to new queue system and off callback handle.
majora2007 Jan 16, 2024
3168302
Fixed bookmarks having + in the filename when not needed.
majora2007 Jan 16, 2024
56ba804
Cleaned up unused variable on Bookmarks page
majora2007 Jan 16, 2024
e2680fc
Fixed a bad localization string for chapter actions from within the c…
majora2007 Jan 16, 2024
d904dc3
Misc code cleanup
majora2007 Jan 16, 2024
809b922
Chapter/volume downloading is working again.
majora2007 Jan 16, 2024
2bf668a
Finished with initial download queue. Not sure if it's fully working …
majora2007 Jan 16, 2024
73c84b1
Added the ability to delete a library from side nav. Reverted some of…
majora2007 Jan 17, 2024
bba05dc
Hooked up bookmark count on the cards (as it somehow got disconnected)
majora2007 Jan 17, 2024
5c8bf86
Added badges to AniList token and Email sections if there are issues …
majora2007 Jan 17, 2024
c117341
A few more prompts to inform users to rotate their AniList keys.
majora2007 Jan 17, 2024
6348bda
Added an additional check for if the sub is active or not. If it's no…
majora2007 Jan 17, 2024
0e7a346
Switched to the new API for Kavita+ which reduces some time
majora2007 Jan 17, 2024
81835a4
A few extra cases where we should reschedule K+ jobs
majora2007 Jan 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion API/Controllers/DownloadController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
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);
}

}
13 changes: 10 additions & 3 deletions API/Controllers/LicenseController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,7 +19,8 @@ public class LicenseController(
IUnitOfWork unitOfWork,
ILogger<LicenseController> logger,
ILicenseService licenseService,
ILocalizationService localizationService)
ILocalizationService localizationService,
ITaskScheduler taskScheduler)
: BaseApiController
{
/// <summary>
Expand All @@ -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>
Expand All @@ -57,6 +62,7 @@ public async Task<ActionResult> RemoveLicense()
setting.Value = null;
unitOfWork.SettingsRepository.Update(setting);
await unitOfWork.CommitAsync();
await taskScheduler.ScheduleKavitaPlusTasks();
return Ok();
}

Expand All @@ -82,6 +88,7 @@ public async Task<ActionResult> UpdateLicense(UpdateLicenseDto dto)
try
{
await licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim(), dto.DiscordId);
await taskScheduler.ScheduleKavitaPlusTasks();
}
catch (Exception ex)
{
Expand Down
77 changes: 52 additions & 25 deletions API/Controllers/MetadataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,22 @@
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;

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>
Expand All @@ -41,10 +36,10 @@ public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? library
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>
Expand All @@ -57,8 +52,8 @@ public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? library
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>
Expand All @@ -73,9 +68,9 @@ public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryId
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>
Expand All @@ -90,9 +85,9 @@ public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
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>
Expand All @@ -108,7 +103,7 @@ public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? li
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()
Expand All @@ -131,7 +126,7 @@ public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? library
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()
Expand All @@ -152,10 +147,13 @@ public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? library
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()
Expand All @@ -177,9 +175,38 @@ public IEnumerable<LanguageDto> GetAllValidLanguages()
[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);

}
}
16 changes: 2 additions & 14 deletions API/Controllers/RatingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,14 @@ public RatingController(ILicenseService licenseService, IRatingService ratingSer
/// <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")]
Expand Down
72 changes: 2 additions & 70 deletions API/Controllers/ReviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,78 +51,10 @@ public ReviewController(ILogger<ReviewController> logger, IUnitOfWork unitOfWork
/// </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>
Expand Down
15 changes: 15 additions & 0 deletions API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using API.DTOs.Recommendation;

namespace API.DTOs.SeriesDetail;

/// <summary>
/// All the data from Kavita+ for Series Detail
/// </summary>
/// <remarks>This is what the UI sees, not what the API sends back</remarks>
public class SeriesDetailPlusDto
{
public RecommendationDto Recommendations { get; set; }
public IEnumerable<UserReviewDto> Reviews { get; set; }
public IEnumerable<RatingDto> Ratings { get; set; }
}
Loading
Loading