From f29c63c6c418ccfeae238b12ca4903a1d3706241 Mon Sep 17 00:00:00 2001 From: Amelia <77553571+Fesaa@users.noreply.github.com> Date: Sat, 26 Apr 2025 20:20:14 +0200 Subject: [PATCH] Unify ChapterRating with Rating --- API.Tests/Extensions/SeriesFilterTests.cs | 4 +- API.Tests/Services/SeriesServiceTests.cs | 10 +- API/Controllers/ChapterController.cs | 16 +--- API/Controllers/RatingController.cs | 43 ++++++--- API/Controllers/ReviewController.cs | 88 +++++------------ API/Controllers/SeriesController.cs | 15 --- API/Controllers/VolumeController.cs | 11 --- API/DTOs/SeriesDetail/UpdateUserReviewDto.cs | 1 + API/DTOs/UpdateChapterRatingDto.cs | 7 -- ...eSeriesRatingDto.cs => UpdateRatingDto.cs} | 3 +- API/Data/DataContext.cs | 1 - .../20250426142952_ChapterRating.cs | 86 ----------------- ... 20250426173850_ChapterRating.Designer.cs} | 96 +++---------------- .../20250426173850_ChapterRating.cs | 48 ++++++++++ .../Migrations/DataContextModelSnapshot.cs | 94 +++--------------- API/Data/Repositories/ChapterRepository.cs | 4 +- API/Data/Repositories/SeriesRepository.cs | 6 +- API/Data/Repositories/UserRepository.cs | 31 +----- API/Entities/AppUser.cs | 1 - API/Entities/AppUserChapterRating.cs | 34 ------- API/Entities/AppUserRating.cs | 3 + API/Entities/Chapter.cs | 2 +- API/Entities/Enums/RatingAuthority.cs | 7 ++ .../QueryExtensions/IncludesExtensions.cs | 5 - API/Helpers/AutoMapperProfiles.cs | 16 ---- API/Helpers/Builders/ChapterRatingBuilder.cs | 58 ----------- API/Services/RatingService.cs | 56 +++++++---- API/Services/SeriesService.cs | 53 ---------- UI/Web/src/app/_services/chapter.service.ts | 16 ---- UI/Web/src/app/_services/review.service.ts | 56 +++++++++++ UI/Web/src/app/_services/series.service.ts | 18 ---- .../review-modal/review-modal.component.ts | 21 +--- .../external-rating.component.ts | 23 ++--- .../volume-detail/volume-detail.component.ts | 12 ++- 34 files changed, 266 insertions(+), 679 deletions(-) delete mode 100644 API/DTOs/UpdateChapterRatingDto.cs rename API/DTOs/{UpdateSeriesRatingDto.cs => UpdateRatingDto.cs} (61%) delete mode 100644 API/Data/Migrations/20250426142952_ChapterRating.cs rename API/Data/Migrations/{20250426142952_ChapterRating.Designer.cs => 20250426173850_ChapterRating.Designer.cs} (97%) create mode 100644 API/Data/Migrations/20250426173850_ChapterRating.cs delete mode 100644 API/Entities/AppUserChapterRating.cs create mode 100644 API/Entities/Enums/RatingAuthority.cs delete mode 100644 API/Helpers/Builders/ChapterRatingBuilder.cs create mode 100644 UI/Web/src/app/_services/review.service.ts diff --git a/API.Tests/Extensions/SeriesFilterTests.cs b/API.Tests/Extensions/SeriesFilterTests.cs index 577e17619..0b517373a 100644 --- a/API.Tests/Extensions/SeriesFilterTests.cs +++ b/API.Tests/Extensions/SeriesFilterTests.cs @@ -939,7 +939,7 @@ public class SeriesFilterTests : AbstractDbTest var zeroRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2); Assert.NotNull(zeroRating); - Assert.True(await seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + Assert.True(await seriesService.UpdateRating(user, new UpdateRatingDto() { SeriesId = zeroRating.Id, UserRating = 0 @@ -948,7 +948,7 @@ public class SeriesFilterTests : AbstractDbTest // Select 4.5 Rating var partialRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(3); - Assert.True(await seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + Assert.True(await seriesService.UpdateRating(user, new UpdateRatingDto() { SeriesId = partialRating.Id, UserRating = 4.5f diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 5696bb76b..6856500e7 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -617,7 +617,7 @@ public class SeriesServiceTests : AbstractDbTest var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); JobStorage.Current = new InMemoryStorage(); - var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto + var result = await _seriesService.UpdateRating(user, new UpdateRatingDto { SeriesId = 1, UserRating = 3, @@ -651,7 +651,7 @@ public class SeriesServiceTests : AbstractDbTest var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto + var result = await _seriesService.UpdateRating(user, new UpdateRatingDto { SeriesId = 1, UserRating = 3, @@ -667,7 +667,7 @@ public class SeriesServiceTests : AbstractDbTest // Update the DB again - var result2 = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto + var result2 = await _seriesService.UpdateRating(user, new UpdateRatingDto { SeriesId = 1, UserRating = 5, @@ -701,7 +701,7 @@ public class SeriesServiceTests : AbstractDbTest var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto + var result = await _seriesService.UpdateRating(user, new UpdateRatingDto { SeriesId = 1, UserRating = 10, @@ -736,7 +736,7 @@ public class SeriesServiceTests : AbstractDbTest var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto + var result = await _seriesService.UpdateRating(user, new UpdateRatingDto { SeriesId = 2, UserRating = 5, diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index d790e8ede..1421cf164 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -28,16 +28,13 @@ public class ChapterController : BaseApiController private readonly ILocalizationService _localizationService; private readonly IEventHub _eventHub; private readonly ILogger _logger; - private readonly IRatingService _ratingService; - public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger, - IRatingService ratingService) + public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger) { _unitOfWork = unitOfWork; _localizationService = localizationService; _eventHub = eventHub; _logger = logger; - _ratingService = ratingService; } /// @@ -406,15 +403,4 @@ public class ChapterController : BaseApiController return await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId()); } - [HttpPost("update-rating")] - public async Task UpdateRating(UpdateChapterRatingDto dto) - { - if (await _ratingService.UpdateChapterRating(User.GetUserId(), dto)) - { - return Ok(); - } - - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); - } - } diff --git a/API/Controllers/RatingController.cs b/API/Controllers/RatingController.cs index 57b448027..9490c41ee 100644 --- a/API/Controllers/RatingController.cs +++ b/API/Controllers/RatingController.cs @@ -6,6 +6,7 @@ using API.Constants; using API.Data; using API.DTOs; using API.Extensions; +using API.Services; using API.Services.Plus; using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; @@ -21,31 +22,45 @@ namespace API.Controllers; public class RatingController : BaseApiController { private readonly IUnitOfWork _unitOfWork; + private readonly IRatingService _ratingService; + private readonly ILocalizationService _localizationService; - public RatingController(IUnitOfWork unitOfWork) + public RatingController(IUnitOfWork unitOfWork, IRatingService ratingService, ILocalizationService localizationService) { _unitOfWork = unitOfWork; + _ratingService = ratingService; + _localizationService = localizationService; + } + [HttpPost] + public async Task UpdateRating(UpdateRatingDto updateRating) + { + if (await _ratingService.UpdateRating(User.GetUserId(), updateRating)) + { + return Ok(); + } + + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); } [HttpGet("overall")] - public async Task> GetOverallRating(int seriesId) + public async Task> GetOverallRating(int seriesId, [FromQuery] int? chapterId) { + int average; + if (chapterId != null) + { + average = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId.Value, User.GetUserId()); + } + else + { + average = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()); + } + + return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()), - FavoriteCount = 0 - }); - } - - [HttpGet("overall/chapter")] - public async Task> GetOverallChapterRating([FromQuery] int chapterId) - { - return Ok(new RatingDto - { - Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, User.GetUserId()), + AverageScore = average, FavoriteCount = 0, }); } diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index 62046ff8d..5d33a9b65 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; @@ -29,9 +30,24 @@ public class ReviewController : BaseApiController _scrobblingService = scrobblingService; } + /// + /// Get all reviews for the series, or chapter + /// + /// + [HttpGet] + public async Task> GetReviews([FromQuery] int seriesId, [FromQuery] int? chapterId) + { + if (chapterId == null) + { + return await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId()); + } + + return await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId.Value, User.GetUserId()); + } + /// - /// Updates the review for a given series + /// Updates the review for a given series, or chapter /// /// /// @@ -41,7 +57,7 @@ public class ReviewController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings); if (user == null) return Unauthorized(); - var ratingBuilder = new RatingBuilder(user.Ratings.FirstOrDefault(r => r.SeriesId == dto.SeriesId)); + var ratingBuilder = new RatingBuilder(await _unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id, dto.ChapterId)); var rating = ratingBuilder .WithBody(dto.Body) @@ -64,78 +80,18 @@ public class ReviewController : BaseApiController return Ok(_mapper.Map(rating)); } - /// - /// Updates the review for a given series - /// - /// - /// - /// - [HttpPost("chapter/{chapterId}")] - public async Task> UpdateChapterReview(int chapterId, UpdateUserReviewDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings); - if (user == null) return Unauthorized(); - - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId, ChapterIncludes.None); - if (chapter == null) return BadRequest(); - - var builder = new ChapterRatingBuilder(user.ChapterRatings.FirstOrDefault(r => r.SeriesId == dto.SeriesId)); - - var rating = builder - .WithSeriesId(dto.SeriesId) - .WithVolumeId(chapter.VolumeId) - .WithChapterId(chapter.Id) - .WithRating(dto.Rating) - .WithReview(dto.Body) - .WithProvider(ScrobbleProvider.Kavita) - .Build(); - - if (rating.Id == 0) - { - user.ChapterRatings.Add(rating); - } - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - - // Do I need this? - //BackgroundJob.Enqueue(() => - // _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); - return Ok(_mapper.Map(rating)); - } - /// - /// Deletes the user's review for the given series + /// Deletes the user's review for the given series, or chapter /// /// [HttpDelete] - public async Task DeleteReview(int seriesId) + public async Task DeleteReview([FromQuery] int seriesId, [FromQuery] int? chapterId) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings); if (user == null) return Unauthorized(); - user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - - return Ok(); - } - - /// - /// Deletes the user's review for a given chapter - /// - /// - /// - [HttpDelete("chapter/{chapterId}")] - public async Task DeleteChapterReview(int chapterId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings); - if (user == null) return Unauthorized(); - - user.ChapterRatings = user.ChapterRatings.Where(c => c.ChapterId != chapterId).ToList(); + user.Ratings = user.Ratings.Where(r => !(r.SeriesId == seriesId && r.ChapterId == chapterId)).ToList(); _unitOfWork.UserRepository.Update(user); diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 94f9c084f..7cd897c32 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -191,21 +191,6 @@ public class SeriesController : BaseApiController return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); } - - /// - /// Update the user rating for the given series - /// - /// - /// - [HttpPost("update-rating")] - public async Task UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings); - if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); - return Ok(); - } - /// /// Updates the Series /// diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs index 0e1f57b0b..94bc1e79c 100644 --- a/API/Controllers/VolumeController.cs +++ b/API/Controllers/VolumeController.cs @@ -83,15 +83,4 @@ public class VolumeController : BaseApiController return Ok(true); } - - /// - /// Returns all reviews related to this volume, that is, the union of reviews of this volumes chapters - /// - /// - /// - [HttpGet("review")] - public async Task> VolumeReviews([FromQuery] int volumeId) - { - return await _unitOfWork.UserRepository.GetUserRatingDtosForVolumeAsync(volumeId, User.GetUserId()); - } } diff --git a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs index c44a36548..36c6fe92d 100644 --- a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs @@ -6,6 +6,7 @@ namespace API.DTOs.SeriesDetail; public class UpdateUserReviewDto { public int SeriesId { get; set; } + public int? ChapterId { get; set; } public int Rating { get; set; } public string Body { get; set; } } diff --git a/API/DTOs/UpdateChapterRatingDto.cs b/API/DTOs/UpdateChapterRatingDto.cs deleted file mode 100644 index df57a5b42..000000000 --- a/API/DTOs/UpdateChapterRatingDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace API.DTOs; - -public class UpdateChapterRatingDto -{ - public int ChapterId { get; init; } - public float Rating { get; init; } -} diff --git a/API/DTOs/UpdateSeriesRatingDto.cs b/API/DTOs/UpdateRatingDto.cs similarity index 61% rename from API/DTOs/UpdateSeriesRatingDto.cs rename to API/DTOs/UpdateRatingDto.cs index 5dafa35af..f462fdc2b 100644 --- a/API/DTOs/UpdateSeriesRatingDto.cs +++ b/API/DTOs/UpdateRatingDto.cs @@ -1,7 +1,8 @@ namespace API.DTOs; -public class UpdateSeriesRatingDto +public class UpdateRatingDto { public int SeriesId { get; init; } + public int? ChapterId { get; init; } public float UserRating { get; init; } } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index d16243ef4..4533a5dbf 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -40,7 +40,6 @@ public sealed class DataContext : IdentityDbContext MangaFile { get; set; } = null!; public DbSet AppUserProgresses { get; set; } = null!; public DbSet AppUserRating { get; set; } = null!; - public DbSet AppUserChapterRating { get; set; } = null!; public DbSet ServerSetting { get; set; } = null!; public DbSet AppUserPreferences { get; set; } = null!; public DbSet SeriesMetadata { get; set; } = null!; diff --git a/API/Data/Migrations/20250426142952_ChapterRating.cs b/API/Data/Migrations/20250426142952_ChapterRating.cs deleted file mode 100644 index 5d7ce939e..000000000 --- a/API/Data/Migrations/20250426142952_ChapterRating.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ChapterRating : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AppUserChapterRating", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Rating = table.Column(type: "REAL", nullable: false), - HasBeenRated = table.Column(type: "INTEGER", nullable: false), - Review = table.Column(type: "TEXT", nullable: true), - Provider = table.Column(type: "INTEGER", nullable: false), - Authority = table.Column(type: "INTEGER", nullable: false), - SeriesId = table.Column(type: "INTEGER", nullable: false), - ChapterId = table.Column(type: "INTEGER", nullable: false), - VolumeId = table.Column(type: "INTEGER", nullable: false), - AppUserId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserChapterRating", x => x.Id); - table.ForeignKey( - name: "FK_AppUserChapterRating_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserChapterRating_Chapter_ChapterId", - column: x => x.ChapterId, - principalTable: "Chapter", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserChapterRating_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserChapterRating_Volume_VolumeId", - column: x => x.VolumeId, - principalTable: "Volume", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_AppUserId", - table: "AppUserChapterRating", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_ChapterId", - table: "AppUserChapterRating", - column: "ChapterId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_SeriesId", - table: "AppUserChapterRating", - column: "SeriesId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_VolumeId", - table: "AppUserChapterRating", - column: "VolumeId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AppUserChapterRating"); - } - } -} diff --git a/API/Data/Migrations/20250426142952_ChapterRating.Designer.cs b/API/Data/Migrations/20250426173850_ChapterRating.Designer.cs similarity index 97% rename from API/Data/Migrations/20250426142952_ChapterRating.Designer.cs rename to API/Data/Migrations/20250426173850_ChapterRating.Designer.cs index 234dad963..0f9f6f439 100644 --- a/API/Data/Migrations/20250426142952_ChapterRating.Designer.cs +++ b/API/Data/Migrations/20250426173850_ChapterRating.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace API.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20250426142952_ChapterRating")] + [Migration("20250426173850_ChapterRating")] partial class ChapterRating { /// @@ -198,52 +198,6 @@ namespace API.Data.Migrations b.ToTable("AppUserBookmark"); }); - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("AppUserChapterRating"); - }); - modelBuilder.Entity("API.Entities.AppUserCollection", b => { b.Property("Id") @@ -599,6 +553,9 @@ namespace API.Data.Migrations b.Property("AppUserId") .HasColumnType("INTEGER"); + b.Property("ChapterId") + .HasColumnType("INTEGER"); + b.Property("HasBeenRated") .HasColumnType("INTEGER"); @@ -618,6 +575,8 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); + b.HasIndex("ChapterId"); + b.HasIndex("SeriesId"); b.ToTable("AppUserRating"); @@ -2667,41 +2626,6 @@ namespace API.Data.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - modelBuilder.Entity("API.Entities.AppUserCollection", b => { b.HasOne("API.Entities.AppUser", "AppUser") @@ -2808,6 +2732,10 @@ namespace API.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId"); + b.HasOne("API.Entities.Series", "Series") .WithMany("Ratings") .HasForeignKey("SeriesId") @@ -2816,6 +2744,8 @@ namespace API.Data.Migrations b.Navigation("AppUser"); + b.Navigation("Chapter"); + b.Navigation("Series"); }); @@ -3416,8 +3346,6 @@ namespace API.Data.Migrations { b.Navigation("Bookmarks"); - b.Navigation("ChapterRatings"); - b.Navigation("Collections"); b.Navigation("DashboardStreams"); diff --git a/API/Data/Migrations/20250426173850_ChapterRating.cs b/API/Data/Migrations/20250426173850_ChapterRating.cs new file mode 100644 index 000000000..146045b96 --- /dev/null +++ b/API/Data/Migrations/20250426173850_ChapterRating.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class ChapterRating : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ChapterId", + table: "AppUserRating", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_AppUserRating_ChapterId", + table: "AppUserRating", + column: "ChapterId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserRating_Chapter_ChapterId", + table: "AppUserRating", + column: "ChapterId", + principalTable: "Chapter", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserRating_Chapter_ChapterId", + table: "AppUserRating"); + + migrationBuilder.DropIndex( + name: "IX_AppUserRating_ChapterId", + table: "AppUserRating"); + + migrationBuilder.DropColumn( + name: "ChapterId", + table: "AppUserRating"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index b63b09f99..bcf39ace2 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -195,52 +195,6 @@ namespace API.Data.Migrations b.ToTable("AppUserBookmark"); }); - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("AppUserChapterRating"); - }); - modelBuilder.Entity("API.Entities.AppUserCollection", b => { b.Property("Id") @@ -596,6 +550,9 @@ namespace API.Data.Migrations b.Property("AppUserId") .HasColumnType("INTEGER"); + b.Property("ChapterId") + .HasColumnType("INTEGER"); + b.Property("HasBeenRated") .HasColumnType("INTEGER"); @@ -615,6 +572,8 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); + b.HasIndex("ChapterId"); + b.HasIndex("SeriesId"); b.ToTable("AppUserRating"); @@ -2664,41 +2623,6 @@ namespace API.Data.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - modelBuilder.Entity("API.Entities.AppUserCollection", b => { b.HasOne("API.Entities.AppUser", "AppUser") @@ -2805,6 +2729,10 @@ namespace API.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId"); + b.HasOne("API.Entities.Series", "Series") .WithMany("Ratings") .HasForeignKey("SeriesId") @@ -2813,6 +2741,8 @@ namespace API.Data.Migrations b.Navigation("AppUser"); + b.Navigation("Chapter"); + b.Navigation("Series"); }); @@ -3413,8 +3343,6 @@ namespace API.Data.Migrations { b.Navigation("Bookmarks"); - b.Navigation("ChapterRatings"); - b.Navigation("Collections"); b.Navigation("DashboardStreams"); diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 9e07c39b1..f8a431d34 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -315,14 +315,14 @@ public class ChapterRepository : IChapterRepository public async Task GetAverageUserRating(int chapterId, int userId) { // If there is 0 or 1 rating and that rating is you, return 0 back - var countOfRatingsThatAreUser = await _context.AppUserChapterRating + var countOfRatingsThatAreUser = await _context.AppUserRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) .CountAsync(u => u.AppUserId == userId); if (countOfRatingsThatAreUser == 1) { return 0; } - var avg = (await _context.AppUserChapterRating + var avg = (await _context.AppUserRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) .AverageAsync(r => (int?) r.Rating)); return avg.HasValue ? (int) (avg.Value * 20) : 0; diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 31ddc22f1..ea1716ef2 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -758,7 +758,7 @@ public class SeriesRepository : ISeriesRepository foreach (var s in series) { s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead); - var rating = userRatings.SingleOrDefault(r => r.SeriesId == s.Id); + var rating = userRatings.SingleOrDefault(r => r.SeriesId == s.Id && r.ChapterId == null); if (rating != null) { s.UserRating = rating.Rating; @@ -2177,14 +2177,14 @@ public class SeriesRepository : ISeriesRepository { // If there is 0 or 1 rating and that rating is you, return 0 back var countOfRatingsThatAreUser = await _context.AppUserRating - .Where(r => r.SeriesId == seriesId && r.HasBeenRated) + .Where(r => r.SeriesId == seriesId && r.HasBeenRated && r.ChapterId == null) .CountAsync(u => u.AppUserId == userId); if (countOfRatingsThatAreUser == 1) { return 0; } var avg = (await _context.AppUserRating - .Where(r => r.SeriesId == seriesId && r.HasBeenRated) + .Where(r => r.SeriesId == seriesId && r.HasBeenRated && r.ChapterId == null) .AverageAsync(r => (int?) r.Rating)); return avg.HasValue ? (int) (avg.Value * 20) : 0; } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 68ea0150f..b04dfbc13 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -43,7 +43,6 @@ public enum AppUserIncludes SideNavStreams = 4096, ExternalSources = 8192, Collections = 16384, // 2^14 - ChapterRatings = 1 << 15, } public interface IUserRepository @@ -65,10 +64,8 @@ public interface IUserRepository Task> GetAdminUsersAsync(); Task IsUserAdminAsync(AppUser? user); Task> GetRoles(int userId); - Task GetUserRatingAsync(int seriesId, int userId); - Task GetUserChapterRatingAsync(int chapterId, int userId); + Task GetUserRatingAsync(int seriesId, int userId, int? chapterId = null); Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId); - Task> GetUserRatingDtosForVolumeAsync(int volumeId, int userId); Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId); Task GetPreferencesAsync(string username); Task> GetBookmarkDtosForSeries(int userId, int seriesId); @@ -587,36 +584,18 @@ public class UserRepository : IUserRepository return await _userManager.GetRolesAsync(user); } - public async Task GetUserRatingAsync(int seriesId, int userId) + public async Task GetUserRatingAsync(int seriesId, int userId, int? chapterId = null) { return await _context.AppUserRating - .Where(r => r.SeriesId == seriesId && r.AppUserId == userId) + .Where(r => r.SeriesId == seriesId && r.AppUserId == userId && r.ChapterId == chapterId) .SingleOrDefaultAsync(); } - public async Task GetUserChapterRatingAsync(int chapterId, int userId) - { - return await _context.AppUserChapterRating - .Where(r => r.ChapterId == chapterId && r.AppUserId == userId) - .FirstOrDefaultAsync(); - } public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) { return await _context.AppUserRating .Include(r => r.AppUser) - .Where(r => r.SeriesId == seriesId) - .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) - .OrderBy(r => r.AppUserId == userId) - .ThenBy(r => r.Rating) - .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - public async Task> GetUserRatingDtosForVolumeAsync(int volumeId, int userId) - { - return await _context.AppUserChapterRating - .Include(r => r.AppUser) - .Where(r => r.VolumeId == volumeId) + .Where(r => r.SeriesId == seriesId && r.ChapterId == null) .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) @@ -627,7 +606,7 @@ public class UserRepository : IUserRepository public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId) { - return await _context.AppUserChapterRating + return await _context.AppUserRating .Include(r => r.AppUser) .Where(r => r.ChapterId == chapterId) .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 50f795041..b95cfd260 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -19,7 +19,6 @@ public class AppUser : IdentityUser, IHasConcurrencyToken public ICollection UserRoles { get; set; } = null!; public ICollection Progresses { get; set; } = null!; public ICollection Ratings { get; set; } = null!; - public ICollection ChapterRatings { get; set; } = null!; public AppUserPreferences UserPreferences { get; set; } = null!; /// /// Bookmarks associated with this User diff --git a/API/Entities/AppUserChapterRating.cs b/API/Entities/AppUserChapterRating.cs deleted file mode 100644 index 8cc3695f5..000000000 --- a/API/Entities/AppUserChapterRating.cs +++ /dev/null @@ -1,34 +0,0 @@ -using API.Entities.Enums; -using API.Services.Plus; - -namespace API.Entities; - -#nullable enable - -public enum ReviewAuthority -{ - User = 0, - Critic = 1, -} - -public class AppUserChapterRating -{ - public int Id { get; set; } - public float Rating { get; set; } - public bool HasBeenRated { get; set; } - public string? Review { get; set; } - public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita; - public ReviewAuthority Authority { get; set; } = ReviewAuthority.User; - - public int SeriesId { get; set; } - public Series Series { get; set; } = null!; - - public int ChapterId { get; set; } - public Chapter Chapter { get; set; } = null!; - - public int VolumeId { get; set; } - public Volume Volume { get; set; } = null!; - - public int AppUserId { get; set; } - public AppUser AppUser { get; set; } = null!; -} diff --git a/API/Entities/AppUserRating.cs b/API/Entities/AppUserRating.cs index 5d66a06e4..c1688b0e4 100644 --- a/API/Entities/AppUserRating.cs +++ b/API/Entities/AppUserRating.cs @@ -26,6 +26,9 @@ public class AppUserRating public int SeriesId { get; set; } public Series Series { get; set; } = null!; + public int? ChapterId { get; set; } = null; + public Chapter? Chapter { get; set; } = null; + // Relationships public int AppUserId { get; set; } diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index 86357f236..c76658c41 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -160,7 +160,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage /// public ICollection Genres { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); - public ICollection Ratings { get; set; } = []; + public ICollection Ratings { get; set; } = []; public ICollection UserProgress { get; set; } diff --git a/API/Entities/Enums/RatingAuthority.cs b/API/Entities/Enums/RatingAuthority.cs new file mode 100644 index 000000000..11ffa47a3 --- /dev/null +++ b/API/Entities/Enums/RatingAuthority.cs @@ -0,0 +1,7 @@ +namespace API.Entities.Enums; + +public enum RatingAuthority +{ + User = 0, + Critic = 1, +} diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index 2c5b77f5a..983f6798e 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -253,11 +253,6 @@ public static class IncludesExtensions .ThenInclude(c => c.Items); } - if (includeFlags.HasFlag(AppUserIncludes.ChapterRatings)) - { - query = query.Include(u => u.ChapterRatings); - } - return query.AsSplitQuery(); } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index b725834b2..69ed884fd 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -97,22 +97,6 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.AppUser.UserName)); - CreateMap() - .ForMember(dest => dest.LibraryId, - opt => - opt.MapFrom(src => src.Series.LibraryId)) - .ForMember(dest => dest.VolumeId, - opt => - opt.MapFrom(src => src.VolumeId)) - .ForMember(dest => dest.ChapterId, - opt => - opt.MapFrom(src => src.ChapterId)) - .ForMember(dest => dest.Body, - opt => - opt.MapFrom(src => src.Review)) - .ForMember(dest => dest.Username, - opt => - opt.MapFrom(src => src.AppUser.UserName)); CreateMap() .ForMember(dest => dest.PageNum, diff --git a/API/Helpers/Builders/ChapterRatingBuilder.cs b/API/Helpers/Builders/ChapterRatingBuilder.cs deleted file mode 100644 index fabc81842..000000000 --- a/API/Helpers/Builders/ChapterRatingBuilder.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using API.Entities; -using API.Entities.Enums; -using API.Services.Plus; - -namespace API.Helpers.Builders; - -#nullable enable -public class ChapterRatingBuilder -{ - private readonly AppUserChapterRating _rating; - - public AppUserChapterRating Build() => _rating; - - public ChapterRatingBuilder(AppUserChapterRating? rating = null) - { - _rating = rating ?? new AppUserChapterRating(); - } - - public ChapterRatingBuilder WithSeriesId(int seriesId) - { - _rating.SeriesId = seriesId; - return this; - } - - public ChapterRatingBuilder WithChapterId(int chapterId) - { - _rating.ChapterId = chapterId; - return this; - } - - public ChapterRatingBuilder WithVolumeId(int volumeId) - { - _rating.VolumeId = volumeId; - return this; - } - - public ChapterRatingBuilder WithRating(int rating) - { - _rating.Rating = Math.Clamp(rating, 0, 5); - _rating.HasBeenRated = true; - return this; - } - - public ChapterRatingBuilder WithReview(string review) - { - _rating.Review = review; - return this; - } - - public ChapterRatingBuilder WithProvider(ScrobbleProvider provider) - { - _rating.Provider = provider; - return this; - } - - -} diff --git a/API/Services/RatingService.cs b/API/Services/RatingService.cs index d327f4a0c..d9504a820 100644 --- a/API/Services/RatingService.cs +++ b/API/Services/RatingService.cs @@ -1,63 +1,81 @@ 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 { - Task UpdateChapterRating(int userId, UpdateChapterRatingDto dto); + Task UpdateRating(int userId, UpdateRatingDto updateRatingDto); } public class RatingService: IRatingService { private readonly IUnitOfWork _unitOfWork; + private readonly IScrobblingService _scrobblingService; private readonly ILogger _logger; - public RatingService(IUnitOfWork unitOfWork, ILogger logger) + public RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger) { _unitOfWork = unitOfWork; + _scrobblingService = scrobblingService; _logger = logger; } - public async Task UpdateChapterRating(int userId, UpdateChapterRatingDto dto) + /// + /// + /// + /// + /// + /// + public async Task UpdateRating(int userId, UpdateRatingDto updateRatingDto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ChapterRatings); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Ratings); if (user == null) throw new UnauthorizedAccessException(); - var rating = await _unitOfWork.UserRepository.GetUserChapterRatingAsync(dto.ChapterId, userId) ?? new AppUserChapterRating(); - - rating.Rating = Math.Clamp(dto.Rating, 0, 5); - rating.HasBeenRated = true; - rating.ChapterId = dto.ChapterId; - - if (rating.Id == 0) - { - user.ChapterRatings.Add(rating); - } - - _unitOfWork.UserRepository.Update(user); + var userRating = + await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id, updateRatingDto.ChapterId) ?? + new AppUserRating(); try { + userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; + userRating.SeriesId = updateRatingDto.SeriesId; + userRating.ChapterId = updateRatingDto.ChapterId; + + if (userRating.Id == 0) + { + user.Ratings ??= new List(); + user.Ratings.Add(userRating); + } + + _unitOfWork.UserRepository.Update(user); + if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) { - // Scrobble Update? + BackgroundJob.Enqueue(() => + _scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId, + userRating.Rating)); return true; } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception while updating chapter rating"); + _logger.LogError(ex, "There was an exception saving rating"); } await _unitOfWork.RollbackAsync(); - user.ChapterRatings.Remove(rating); + user.Ratings?.Remove(userRating); + return false; } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 6a68f2155..b51ed2df6 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -29,13 +29,11 @@ public interface ISeriesService { Task GetSeriesDetail(int seriesId, int userId); Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto); - Task UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto); Task DeleteMultipleSeries(IList seriesIds); Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto); Task GetRelatedSeries(int userId, int seriesId); Task FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true); Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true); - Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash); Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false); @@ -447,57 +445,6 @@ public class SeriesService : ISeriesService } - - /// - /// - /// - /// User with Ratings includes - /// - /// - public async Task 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(); - 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 DeleteMultipleSeries(IList seriesIds) { try diff --git a/UI/Web/src/app/_services/chapter.service.ts b/UI/Web/src/app/_services/chapter.service.ts index 39855536d..3b7aa0c43 100644 --- a/UI/Web/src/app/_services/chapter.service.ts +++ b/UI/Web/src/app/_services/chapter.service.ts @@ -35,20 +35,4 @@ export class ChapterService { return this.httpClient.get>(this.baseUrl + 'chapter/review?chapterId='+chapterId); } - updateChapterReview(seriesId: number, chapterId: number, body: string, rating: number) { - return this.httpClient.post(this.baseUrl + 'review/chapter/'+chapterId, {seriesId, rating, body}); - } - - deleteChapterReview(chapterId: number) { - return this.httpClient.delete(this.baseUrl + 'review/chapter/'+chapterId); - } - - overallRating(chapterId: number) { - return this.httpClient.get(this.baseUrl + 'rating/overall?chapterId='+chapterId); - } - - updateRating(chapterId: number, rating: number) { - return this.httpClient.post(this.baseUrl + 'chapter/update-rating', {chapterId, rating}); - } - } diff --git a/UI/Web/src/app/_services/review.service.ts b/UI/Web/src/app/_services/review.service.ts new file mode 100644 index 000000000..5667d5dab --- /dev/null +++ b/UI/Web/src/app/_services/review.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import {UserReview} from "../_single-module/review-card/user-review"; +import {environment} from "../../environments/environment"; +import {HttpClient} from "@angular/common/http"; +import {Rating} from "../_models/rating"; + +@Injectable({ + providedIn: 'root' +}) +export class ReviewService { + + private baseUrl = environment.apiUrl; + + constructor(private httpClient: HttpClient) { } + + getReviews(seriesId: number, chapterId?: number) { + if (chapterId) { + return this.httpClient.get(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`); + } + return this.httpClient.get(this.baseUrl + 'review?seriesId=' + seriesId); + } + + deleteReview(seriesId: number, chapterId?: number) { + if (chapterId) { + return this.httpClient.delete(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`); + } + + return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId); + } + + updateReview(seriesId: number, body: string, rating: number, chapterId?: number) { + if (chapterId) { + return this.httpClient.post(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`, { + rating, body + }); + } + + return this.httpClient.post(this.baseUrl + 'review', { + seriesId, rating, body + }); + } + + updateRating(seriesId: number, userRating: number, chapterId?: number) { + return this.httpClient.post(this.baseUrl + 'rating', { + seriesId, chapterId, userRating + }) + } + + overallRating(seriesId: number, chapterId?: number) { + if (chapterId) { + return this.httpClient.get(this.baseUrl + `rating/overall?chapterId=${chapterId}&seriesId=${seriesId}`); + } + return this.httpClient.get(this.baseUrl + 'rating/overall?seriesId=' + seriesId); + } + +} diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 83c80d0fa..b440b1eb7 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -203,27 +203,9 @@ export class SeriesService { return this.httpClient.get(this.baseUrl + 'series/series-detail?seriesId=' + seriesId); } - - - deleteReview(seriesId: number) { - return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId); - } - updateReview(seriesId: number, body: string, rating: number) { - return this.httpClient.post(this.baseUrl + 'review', { - seriesId, rating, body - }); - } - - getReviews(seriesId: number) { - return this.httpClient.get>(this.baseUrl + 'review?seriesId=' + seriesId); - } - getRatings(seriesId: number) { return this.httpClient.get>(this.baseUrl + 'rating?seriesId=' + seriesId); } - getOverallRating(seriesId: number) { - return this.httpClient.get(this.baseUrl + 'rating/overall?seriesId=' + seriesId); - } removeFromOnDeck(seriesId: number) { return this.httpClient.post(this.baseUrl + 'series/remove-from-on-deck?seriesId=' + seriesId, {}); diff --git a/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts b/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts index 25a7f645d..c9f2eb6bf 100644 --- a/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts +++ b/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts @@ -10,6 +10,7 @@ import {ChapterService} from "../../_services/chapter.service"; import {of} from "rxjs"; import {NgxStarsModule} from "ngx-stars"; import {ThemeService} from "../../_services/theme.service"; +import {ReviewService} from "../../_services/review.service"; export enum ReviewModalCloseAction { Create, @@ -33,8 +34,8 @@ export interface ReviewModalCloseEvent { export class ReviewModalComponent implements OnInit { protected readonly modal = inject(NgbActiveModal); + private readonly reviewService = inject(ReviewService); private readonly seriesService = inject(SeriesService); - private readonly chapterService = inject(ChapterService); private readonly cdRef = inject(ChangeDetectorRef); private readonly confirmService = inject(ConfirmService); private readonly toastr = inject(ToastrService); @@ -66,14 +67,7 @@ export class ReviewModalComponent implements OnInit { async delete() { if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return; - let obs; - if (!this.review.chapterId) { - obs = this.seriesService.deleteReview(this.review.seriesId); - } else { - obs = this.chapterService.deleteChapterReview(this.review.chapterId) - } - - obs?.subscribe(() => { + this.reviewService.deleteReview(this.review.seriesId, this.review.chapterId).subscribe(() => { this.toastr.success(translate('toasts.review-deleted')); this.modal.close({success: true, review: this.review, action: ReviewModalCloseAction.Delete}); }); @@ -85,14 +79,7 @@ export class ReviewModalComponent implements OnInit { return; } - let obs; - if (!this.review.chapterId) { - obs = this.seriesService.updateReview(this.review.seriesId, model.reviewBody, this.rating); - } else { - obs = this.chapterService.updateChapterReview(this.review.seriesId, this.review.chapterId, model.reviewBody, this.rating); - } - - obs?.subscribe(review => { + this.reviewService.updateReview(this.review.seriesId, model.reviewBody, this.rating, this.review.chapterId).subscribe(review => { this.modal.close({success: true, review: review, action: ReviewModalCloseAction.Edit}); }); diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index 5106741c6..945ef05ba 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -25,6 +25,7 @@ import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common"; import {RatingModalComponent} from "../rating-modal/rating-modal.component"; import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe"; import {ChapterService} from "../../../_services/chapter.service"; +import {ReviewService} from "../../../_services/review.service"; @Component({ selector: 'app-external-rating', @@ -38,8 +39,7 @@ import {ChapterService} from "../../../_services/chapter.service"; export class ExternalRatingComponent implements OnInit { private readonly cdRef = inject(ChangeDetectorRef); - private readonly seriesService = inject(SeriesService); - private readonly chapterService = inject(ChapterService); + private readonly reviewService = inject(ReviewService); private readonly themeService = inject(ThemeService); public readonly utilityService = inject(UtilityService); public readonly destroyRef = inject(DestroyRef); @@ -61,24 +61,13 @@ export class ExternalRatingComponent implements OnInit { starColor = this.themeService.getCssVariable('--rating-star-color'); ngOnInit() { - let obs; - if (this.chapterId) { - obs = this.chapterService.overallRating(this.chapterId); - } else { - obs = this.seriesService.getOverallRating(this.seriesId); - } - obs?.subscribe(r => this.overallRating = r.averageScore); + this.reviewService.overallRating(this.seriesId, this.chapterId).subscribe(r => { + this.overallRating = r.averageScore; + }); } updateRating(rating: number) { - let obs; - if (this.chapterId) { - obs = this.chapterService.updateRating(this.chapterId, rating); - } else { - obs = this.seriesService.updateRating(this.seriesId, rating); - } - - obs?.subscribe(() => { + this.reviewService.updateRating(this.seriesId, rating, this.chapterId).subscribe(() => { this.userRating = rating; this.hasUserRated = true; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.ts b/UI/Web/src/app/volume-detail/volume-detail.component.ts index 77ccc7f6e..0c573b1ea 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -82,6 +82,7 @@ import {UserReview} from "../_single-module/review-card/user-review"; import {ReviewsComponent} from "../_single-module/reviews/reviews.component"; import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component"; import {User} from "../_models/user"; +import {ReviewService} from "../_services/review.service"; enum TabID { @@ -182,6 +183,7 @@ export class VolumeDetailComponent implements OnInit { private readonly readingListService = inject(ReadingListService); private readonly messageHub = inject(MessageHubService); private readonly location = inject(Location); + private readonly reviewService = inject(ReviewService); protected readonly AgeRating = AgeRating; @@ -391,7 +393,6 @@ export class VolumeDetailComponent implements OnInit { series: this.seriesService.getSeries(this.seriesId), volume: this.volumeService.getVolumeMetadata(this.volumeId), libraryType: this.libraryService.getLibraryType(this.libraryId), - reviews: this.volumeService.volumeReviews(this.volumeId), }).subscribe(results => { if (results.volume === null) { @@ -402,8 +403,13 @@ export class VolumeDetailComponent implements OnInit { this.series = results.series; this.volume = results.volume; this.libraryType = results.libraryType; - this.userReviews = results.reviews.filter(r => !r.isExternal); - this.plusReviews = results.reviews.filter(r => r.isExternal); + + if (this.volume.chapters.length === 1) { + this.reviewService.getReviews(this.seriesId, this.volume.chapters[0].id).subscribe(reviews => { + this.userReviews = reviews.filter(r => !r.isExternal); + this.plusReviews = reviews.filter(r => r.isExternal); + }); + } this.themeService.setColorScape(this.volume!.primaryColor, this.volume!.secondaryColor);