Chapter/Issue level Reviews and Ratings (#3778)
Co-authored-by: Joseph Milazzo <josephmajora@gmail.com>
This commit is contained in:
parent
3b8997e46e
commit
4f7625ea77
60 changed files with 5097 additions and 497 deletions
|
@ -1085,15 +1085,97 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification;
|
||||
|
||||
madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification;
|
||||
madeModification = UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification;
|
||||
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
return madeModification;
|
||||
}
|
||||
|
||||
private bool UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata)
|
||||
{
|
||||
if (!settings.Enabled) return false;
|
||||
|
||||
if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var madeModification = false;
|
||||
|
||||
#region Review
|
||||
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews);
|
||||
List<ExternalReview> externalReviews = [];
|
||||
externalReviews.AddRange(metadata.CriticReviews
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
|
||||
.Select(r =>
|
||||
{
|
||||
var review = _mapper.Map<ExternalReview>(r);
|
||||
review.ChapterId = chapter.Id;
|
||||
review.Authority = RatingAuthority.Critic;
|
||||
CleanCbrReview(ref review);
|
||||
return review;
|
||||
}));
|
||||
externalReviews.AddRange(metadata.UserReviews
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
|
||||
.Select(r =>
|
||||
{
|
||||
var review = _mapper.Map<ExternalReview>(r);
|
||||
review.ChapterId = chapter.Id;
|
||||
review.Authority = RatingAuthority.User;
|
||||
CleanCbrReview(ref review);
|
||||
return review;
|
||||
}));
|
||||
|
||||
chapter.ExternalReviews = externalReviews;
|
||||
madeModification = externalReviews.Count > 0;
|
||||
_logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id);
|
||||
#endregion
|
||||
|
||||
#region Rating
|
||||
|
||||
var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating);
|
||||
var averageUserRating = metadata.UserReviews.Average(r => r.Rating);
|
||||
|
||||
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalRatings);
|
||||
chapter.ExternalRatings =
|
||||
[
|
||||
new ExternalRating
|
||||
{
|
||||
AverageScore = (int) averageUserRating,
|
||||
Provider = ScrobbleProvider.Cbr,
|
||||
Authority = RatingAuthority.User,
|
||||
ProviderUrl = metadata.IssueUrl,
|
||||
},
|
||||
new ExternalRating
|
||||
{
|
||||
AverageScore = (int) averageCriticRating,
|
||||
Provider = ScrobbleProvider.Cbr,
|
||||
Authority = RatingAuthority.Critic,
|
||||
ProviderUrl = metadata.IssueUrl,
|
||||
|
||||
},
|
||||
];
|
||||
|
||||
chapter.AverageExternalRating = averageUserRating;
|
||||
|
||||
madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification;
|
||||
|
||||
#endregion
|
||||
|
||||
return madeModification;
|
||||
}
|
||||
|
||||
private static void CleanCbrReview(ref ExternalReview review)
|
||||
{
|
||||
// CBR has Read Full Review which links to site, but we already have that
|
||||
review.Body = review.Body.Replace("Read Full Review", string.Empty).TrimEnd();
|
||||
review.RawBody = review.RawBody.Replace("Read Full Review", string.Empty).TrimEnd();
|
||||
review.BodyJustText = review.BodyJustText.Replace("Read Full Review", string.Empty).TrimEnd();
|
||||
}
|
||||
|
||||
|
||||
private static bool UpdateChapterSummary(Chapter chapter, MetadataSettingsDto settings, string? summary)
|
||||
{
|
||||
|
|
126
API/Services/RatingService.cs
Normal file
126
API/Services/RatingService.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Services.Plus;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public interface IRatingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the users' rating for a given series
|
||||
/// </summary>
|
||||
/// <param name="user">Should include ratings</param>
|
||||
/// <param name="updateRatingDto"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the users' rating for a given chapter
|
||||
/// </summary>
|
||||
/// <param name="user">Should include ratings</param>
|
||||
/// <param name="updateRatingDto">chapterId must be set</param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto);
|
||||
}
|
||||
|
||||
public class RatingService: IRatingService
|
||||
{
|
||||
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILogger<RatingService> _logger;
|
||||
|
||||
public RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger<RatingService> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_scrobblingService = scrobblingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto)
|
||||
{
|
||||
var userRating =
|
||||
await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id) ??
|
||||
new AppUserRating();
|
||||
|
||||
try
|
||||
{
|
||||
userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f);
|
||||
userRating.HasBeenRated = true;
|
||||
userRating.SeriesId = updateRatingDto.SeriesId;
|
||||
|
||||
if (userRating.Id == 0)
|
||||
{
|
||||
user.Ratings ??= new List<AppUserRating>();
|
||||
user.Ratings.Add(userRating);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||
{
|
||||
BackgroundJob.Enqueue(() =>
|
||||
_scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId,
|
||||
userRating.Rating));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception saving rating");
|
||||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
user.Ratings?.Remove(userRating);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto)
|
||||
{
|
||||
if (updateRatingDto.ChapterId == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var userRating =
|
||||
await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, updateRatingDto.ChapterId.Value) ??
|
||||
new AppUserChapterRating();
|
||||
|
||||
try
|
||||
{
|
||||
userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f);
|
||||
userRating.HasBeenRated = true;
|
||||
userRating.SeriesId = updateRatingDto.SeriesId;
|
||||
userRating.ChapterId = updateRatingDto.ChapterId.Value;
|
||||
|
||||
if (userRating.Id == 0)
|
||||
{
|
||||
user.ChapterRatings ??= new List<AppUserChapterRating>();
|
||||
user.ChapterRatings.Add(userRating);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception saving rating");
|
||||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
user.ChapterRatings?.Remove(userRating);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -29,13 +29,11 @@ public interface ISeriesService
|
|||
{
|
||||
Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId);
|
||||
Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto);
|
||||
Task<bool> UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto);
|
||||
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
|
||||
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true);
|
||||
Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true);
|
||||
|
||||
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle,
|
||||
bool withHash);
|
||||
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
|
||||
|
@ -447,57 +445,6 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="user">User with Ratings includes</param>
|
||||
/// <param name="updateSeriesRatingDto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> UpdateRating(AppUser? user, UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogError("Cannot update rating of null user");
|
||||
return false;
|
||||
}
|
||||
|
||||
var userRating =
|
||||
await _unitOfWork.UserRepository.GetUserRatingAsync(updateSeriesRatingDto.SeriesId, user.Id) ??
|
||||
new AppUserRating();
|
||||
try
|
||||
{
|
||||
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0f, 5f);
|
||||
userRating.HasBeenRated = true;
|
||||
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
||||
|
||||
if (userRating.Id == 0)
|
||||
{
|
||||
user.Ratings ??= new List<AppUserRating>();
|
||||
user.Ratings.Add(userRating);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||
{
|
||||
BackgroundJob.Enqueue(() =>
|
||||
_scrobblingService.ScrobbleRatingUpdate(user.Id, updateSeriesRatingDto.SeriesId,
|
||||
userRating.Rating));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception saving rating");
|
||||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
user.Ratings?.Remove(userRating);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteMultipleSeries(IList<int> seriesIds)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -45,6 +45,7 @@ public class CoverDbService : ICoverDbService
|
|||
private readonly IImageService _imageService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private TimeSpan _cacheTime = TimeSpan.FromDays(10);
|
||||
|
||||
private const string NewHost = "https://www.kavitareader.com/CoversDB/";
|
||||
|
||||
|
@ -97,7 +98,7 @@ public class CoverDbService : ICoverDbService
|
|||
throw new KavitaException($"Kavita has already tried to fetch from {sanitizedBaseUrl} and failed. Skipping duplicate check");
|
||||
}
|
||||
|
||||
await provider.SetAsync(baseUrl, string.Empty, TimeSpan.FromDays(10));
|
||||
await provider.SetAsync(baseUrl, string.Empty, _cacheTime);
|
||||
if (FaviconUrlMapper.TryGetValue(baseUrl, out var value))
|
||||
{
|
||||
url = value;
|
||||
|
@ -185,6 +186,17 @@ public class CoverDbService : ICoverDbService
|
|||
{
|
||||
try
|
||||
{
|
||||
// Sanitize user input
|
||||
publisherName = publisherName.Replace(Environment.NewLine, string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty);
|
||||
var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Publisher);
|
||||
var res = await provider.GetAsync<string>(publisherName);
|
||||
if (res.HasValue)
|
||||
{
|
||||
_logger.LogInformation("Kavita has already tried to fetch Publisher: {PublisherName} and failed. Skipping duplicate check", publisherName);
|
||||
throw new KavitaException($"Kavita has already tried to fetch Publisher: {publisherName} and failed. Skipping duplicate check");
|
||||
}
|
||||
|
||||
await provider.SetAsync(publisherName, string.Empty, _cacheTime);
|
||||
var publisherLink = await FallbackToKavitaReaderPublisher(publisherName);
|
||||
if (string.IsNullOrEmpty(publisherLink))
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue