From 052b3f9fe4009062ffda522f91b00fe2cb6614cd Mon Sep 17 00:00:00 2001 From: Amelia <77553571+Fesaa@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:19:03 +0200 Subject: [PATCH] Ingest ExternalReviews from K+ Adds a new entity ExternalChapterMetadata, which would allow us to extend chapters to Recommendations, Ratings, etc in the future --- API/Controllers/ChapterController.cs | 42 ++++-- API/Controllers/ReviewController.cs | 17 +-- API/DTOs/ChapterDetailPlusDto.cs | 14 ++ API/DTOs/RatingDto.cs | 4 +- API/DTOs/SeriesDetail/UpdateUserReviewDto.cs | 4 +- API/DTOs/SeriesDetail/UserReviewDto.cs | 3 +- API/Data/DataContext.cs | 2 + .../20250426173850_ChapterRating.cs | 48 ------- ... 20250428141809_ChapterRating.Designer.cs} | 107 +++++++++++++- .../20250428141809_ChapterRating.cs | 135 ++++++++++++++++++ .../Migrations/DataContextModelSnapshot.cs | 105 ++++++++++++++ .../ExternalChapterMetadataRepository.cs | 45 ++++++ API/Data/UnitOfWork.cs | 3 + API/Entities/Chapter.cs | 3 + .../Metadata/ExternalChapterMetadata.cs | 20 +++ .../Metadata/ExternalChapterReview.cs | 45 ++++++ .../QueryExtensions/IncludesExtensions.cs | 11 ++ API/Helpers/AutoMapperProfiles.cs | 8 ++ API/Helpers/Builders/RatingBuilder.cs | 6 + API/Services/Plus/ExternalMetadataService.cs | 62 +++++++- UI/Web/src/app/_models/chapter-detail.ts | 9 ++ UI/Web/src/app/_services/chapter.service.ts | 5 +- UI/Web/src/app/_services/review.service.ts | 15 +- .../_single-module/review-card/user-review.ts | 9 +- .../review-modal/review-modal.component.ts | 12 +- .../chapter-detail.component.html | 5 +- .../chapter-detail.component.ts | 20 +-- .../volume-detail.component.html | 5 +- .../volume-detail/volume-detail.component.ts | 20 +-- 29 files changed, 647 insertions(+), 137 deletions(-) create mode 100644 API/DTOs/ChapterDetailPlusDto.cs delete mode 100644 API/Data/Migrations/20250426173850_ChapterRating.cs rename API/Data/Migrations/{20250426173850_ChapterRating.Designer.cs => 20250428141809_ChapterRating.Designer.cs} (96%) create mode 100644 API/Data/Migrations/20250428141809_ChapterRating.cs create mode 100644 API/Data/Repositories/ExternalChapterMetadataRepository.cs create mode 100644 API/Entities/Metadata/ExternalChapterMetadata.cs create mode 100644 API/Entities/Metadata/ExternalChapterReview.cs create mode 100644 UI/Web/src/app/_models/chapter-detail.ts diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index 1421cf164..7634eef25 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -15,8 +15,10 @@ using API.Helpers; using API.Services; using API.Services.Tasks.Scanner.Parser; using API.SignalR; +using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Nager.ArticleNumber; @@ -28,13 +30,16 @@ public class ChapterController : BaseApiController private readonly ILocalizationService _localizationService; private readonly IEventHub _eventHub; private readonly ILogger _logger; + private readonly IMapper _mapper; - public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger) + public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger, + IMapper mapper) { _unitOfWork = unitOfWork; _localizationService = localizationService; _eventHub = eventHub; _logger = logger; + _mapper = mapper; } /// @@ -392,15 +397,34 @@ public class ChapterController : BaseApiController return Ok(); } - /// - /// Get all reviews for this chapter - /// - /// - /// - [HttpGet("review")] - public async Task> ChapterReviews([FromQuery] int chapterId) + + [HttpGet("chapter-detail-plus")] + public async Task ChapterDetailPlus([FromQuery] int seriesId, [FromQuery] int chapterId) { - return await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId()); + var ret = new ChapterDetailPlusDto(); + + var userReviews = (await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId())) + .Where(r => !string.IsNullOrEmpty(r.Body)) + .OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0) + .ToList(); + + var ownRating = await _unitOfWork.UserRepository.GetUserRatingAsync(seriesId, User.GetUserId(), chapterId); + if (ownRating != null) + { + ret.Rating = ownRating.Rating; + ret.HasBeenRated = ownRating.HasBeenRated; + } + + var externalMetadata = await _unitOfWork.ExternalChapterMetadataRepository.Get(chapterId); + if (externalMetadata != null && externalMetadata.ExternalReviews.Count > 0) + { + var dtos = externalMetadata.ExternalReviews.Select(ex => _mapper.Map(ex)).ToList(); + userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(dtos)); + } + + ret.Reviews = userReviews; + + return ret; } } diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index 5d33a9b65..5de31451e 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -30,21 +30,6 @@ 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, or chapter @@ -62,8 +47,8 @@ public class ReviewController : BaseApiController var rating = ratingBuilder .WithBody(dto.Body) .WithSeriesId(dto.SeriesId) + .WithChapterId(dto.ChapterId) .WithTagline(string.Empty) - .WithRating(dto.Rating) .Build(); if (rating.Id == 0) diff --git a/API/DTOs/ChapterDetailPlusDto.cs b/API/DTOs/ChapterDetailPlusDto.cs new file mode 100644 index 000000000..24649edaa --- /dev/null +++ b/API/DTOs/ChapterDetailPlusDto.cs @@ -0,0 +1,14 @@ +#nullable enable +using System.Collections.Generic; +using API.DTOs.SeriesDetail; + +namespace API.DTOs; + +public class ChapterDetailPlusDto +{ + public float Rating { get; set; } + public bool HasBeenRated { get; set; } + + public List Reviews { get; set; } + public List? Ratings { get; set; } +} diff --git a/API/DTOs/RatingDto.cs b/API/DTOs/RatingDto.cs index e2cd9d342..264d2d43c 100644 --- a/API/DTOs/RatingDto.cs +++ b/API/DTOs/RatingDto.cs @@ -1,4 +1,5 @@ -using API.Services.Plus; +using API.Entities.Enums; +using API.Services.Plus; namespace API.DTOs; #nullable enable @@ -8,5 +9,6 @@ public class RatingDto public int AverageScore { get; set; } public int FavoriteCount { get; set; } public ScrobbleProvider Provider { get; set; } + public RatingAuthority Authority { get; set; } = RatingAuthority.User; public string? ProviderUrl { get; set; } } diff --git a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs index 36c6fe92d..adff04d6c 100644 --- a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; - + namespace API.DTOs.SeriesDetail; #nullable enable @@ -7,6 +6,5 @@ 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/SeriesDetail/UserReviewDto.cs b/API/DTOs/SeriesDetail/UserReviewDto.cs index e72c19486..afa8802f8 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UserReviewDto.cs @@ -1,4 +1,5 @@ using API.Entities; +using API.Entities.Enums; using API.Services.Plus; namespace API.DTOs.SeriesDetail; @@ -38,7 +39,6 @@ public class UserReviewDto /// public string Username { get; set; } public int TotalVotes { get; set; } - public float Rating { get; set; } public bool HasBeenRated { get; set; } public string? RawBody { get; set; } /// @@ -58,4 +58,5 @@ public class UserReviewDto /// If this review is External, which Provider did it come from /// public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita; + public RatingAuthority Authority { get; set; } = RatingAuthority.User; } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 4533a5dbf..5d1a1f4b2 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -78,6 +78,8 @@ public sealed class DataContext : IdentityDbContext EmailHistory { get; set; } = null!; public DbSet MetadataSettings { get; set; } = null!; public DbSet MetadataFieldMapping { get; set; } = null!; + public DbSet ExternalChapterReview { get; set; } = null!; + public DbSet ExternalChapterMetadata { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) { diff --git a/API/Data/Migrations/20250426173850_ChapterRating.cs b/API/Data/Migrations/20250426173850_ChapterRating.cs deleted file mode 100644 index 146045b96..000000000 --- a/API/Data/Migrations/20250426173850_ChapterRating.cs +++ /dev/null @@ -1,48 +0,0 @@ -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/20250426173850_ChapterRating.Designer.cs b/API/Data/Migrations/20250428141809_ChapterRating.Designer.cs similarity index 96% rename from API/Data/Migrations/20250426173850_ChapterRating.Designer.cs rename to API/Data/Migrations/20250428141809_ChapterRating.Designer.cs index 0f9f6f439..4b686aba4 100644 --- a/API/Data/Migrations/20250426173850_ChapterRating.Designer.cs +++ b/API/Data/Migrations/20250428141809_ChapterRating.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace API.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20250426173850_ChapterRating")] + [Migration("20250428141809_ChapterRating")] partial class ChapterRating { /// @@ -1318,6 +1318,70 @@ namespace API.Data.Migrations b.ToTable("MediaError"); }); + modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId") + .IsUnique(); + + b.ToTable("ExternalChapterMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalChapterReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalChapterReview"); + }); + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => { b.Property("Id") @@ -2456,6 +2520,21 @@ namespace API.Data.Migrations b.ToTable("CollectionTagSeriesMetadata"); }); + modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b => + { + b.Property("ExternalChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalChapterMetadatasId", "ExternalReviewsId"); + + b.HasIndex("ExternalReviewsId"); + + b.ToTable("ExternalChapterMetadataExternalChapterReview"); + }); + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => { b.Property("ExternalRatingsId") @@ -2919,6 +2998,15 @@ namespace API.Data.Migrations b.Navigation("Chapter"); }); + modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithOne("ExternalChapterMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalChapterMetadata", "ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => { b.HasOne("API.Entities.Series", "Series") @@ -3226,6 +3314,21 @@ namespace API.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b => + { + b.HasOne("API.Entities.Metadata.ExternalChapterMetadata", null) + .WithMany() + .HasForeignKey("ExternalChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalChapterReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => { b.HasOne("API.Entities.Metadata.ExternalRating", null) @@ -3377,6 +3480,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Chapter", b => { + b.Navigation("ExternalChapterMetadata"); + b.Navigation("Files"); b.Navigation("People"); diff --git a/API/Data/Migrations/20250428141809_ChapterRating.cs b/API/Data/Migrations/20250428141809_ChapterRating.cs new file mode 100644 index 000000000..bf9bd497b --- /dev/null +++ b/API/Data/Migrations/20250428141809_ChapterRating.cs @@ -0,0 +1,135 @@ +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.CreateTable( + name: "ExternalChapterMetadata", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChapterId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalChapterMetadata", x => x.Id); + table.ForeignKey( + name: "FK_ExternalChapterMetadata_Chapter_ChapterId", + column: x => x.ChapterId, + principalTable: "Chapter", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExternalChapterReview", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Tagline = table.Column(type: "TEXT", nullable: true), + Body = table.Column(type: "TEXT", nullable: true), + BodyJustText = table.Column(type: "TEXT", nullable: true), + RawBody = table.Column(type: "TEXT", nullable: true), + Provider = table.Column(type: "INTEGER", nullable: false), + Authority = table.Column(type: "INTEGER", nullable: false), + SiteUrl = table.Column(type: "TEXT", nullable: true), + Username = table.Column(type: "TEXT", nullable: true), + Rating = table.Column(type: "INTEGER", nullable: false), + Score = table.Column(type: "INTEGER", nullable: false), + TotalVotes = table.Column(type: "INTEGER", nullable: false), + ChapterId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalChapterReview", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ExternalChapterMetadataExternalChapterReview", + columns: table => new + { + ExternalChapterMetadatasId = table.Column(type: "INTEGER", nullable: false), + ExternalReviewsId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalChapterMetadataExternalChapterReview", x => new { x.ExternalChapterMetadatasId, x.ExternalReviewsId }); + table.ForeignKey( + name: "FK_ExternalChapterMetadataExternalChapterReview_ExternalChapterMetadata_ExternalChapterMetadatasId", + column: x => x.ExternalChapterMetadatasId, + principalTable: "ExternalChapterMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExternalChapterMetadataExternalChapterReview_ExternalChapterReview_ExternalReviewsId", + column: x => x.ExternalReviewsId, + principalTable: "ExternalChapterReview", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserRating_ChapterId", + table: "AppUserRating", + column: "ChapterId"); + + migrationBuilder.CreateIndex( + name: "IX_ExternalChapterMetadata_ChapterId", + table: "ExternalChapterMetadata", + column: "ChapterId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ExternalChapterMetadataExternalChapterReview_ExternalReviewsId", + table: "ExternalChapterMetadataExternalChapterReview", + column: "ExternalReviewsId"); + + 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.DropTable( + name: "ExternalChapterMetadataExternalChapterReview"); + + migrationBuilder.DropTable( + name: "ExternalChapterMetadata"); + + migrationBuilder.DropTable( + name: "ExternalChapterReview"); + + 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 bcf39ace2..1d8323ce4 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1315,6 +1315,70 @@ namespace API.Data.Migrations b.ToTable("MediaError"); }); + modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId") + .IsUnique(); + + b.ToTable("ExternalChapterMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalChapterReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalChapterReview"); + }); + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => { b.Property("Id") @@ -2453,6 +2517,21 @@ namespace API.Data.Migrations b.ToTable("CollectionTagSeriesMetadata"); }); + modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b => + { + b.Property("ExternalChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalChapterMetadatasId", "ExternalReviewsId"); + + b.HasIndex("ExternalReviewsId"); + + b.ToTable("ExternalChapterMetadataExternalChapterReview"); + }); + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => { b.Property("ExternalRatingsId") @@ -2916,6 +2995,15 @@ namespace API.Data.Migrations b.Navigation("Chapter"); }); + modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithOne("ExternalChapterMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalChapterMetadata", "ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => { b.HasOne("API.Entities.Series", "Series") @@ -3223,6 +3311,21 @@ namespace API.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b => + { + b.HasOne("API.Entities.Metadata.ExternalChapterMetadata", null) + .WithMany() + .HasForeignKey("ExternalChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalChapterReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => { b.HasOne("API.Entities.Metadata.ExternalRating", null) @@ -3374,6 +3477,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Chapter", b => { + b.Navigation("ExternalChapterMetadata"); + b.Navigation("Files"); b.Navigation("People"); diff --git a/API/Data/Repositories/ExternalChapterMetadataRepository.cs b/API/Data/Repositories/ExternalChapterMetadataRepository.cs new file mode 100644 index 000000000..7980f31c6 --- /dev/null +++ b/API/Data/Repositories/ExternalChapterMetadataRepository.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using API.Entities.Metadata; +using API.Extensions.QueryExtensions; +using AutoMapper; +using Microsoft.EntityFrameworkCore; + +namespace API.Data.Repositories; + +public enum ExternalChapterMetadataIncludes +{ + None = 0, + ExternalReviews = 1 << 1, +} + +public interface IExternalChapterMetadataRepository +{ + void Attach(ExternalChapterMetadata externalChapterMetadata); + void Remove(IEnumerable? reviews); + + Task Get(int chapterId, ExternalChapterMetadataIncludes includes = ExternalChapterMetadataIncludes.ExternalReviews); +} + +public class ExternalChapterMetadataRepository(DataContext context, IMapper mapper): IExternalChapterMetadataRepository +{ + + public void Attach(ExternalChapterMetadata externalChapterMetadata) + { + context.ExternalChapterMetadata.Attach(externalChapterMetadata); + } + public void Remove(IEnumerable? reviews) + { + if (reviews == null) return; + context.ExternalChapterReview.RemoveRange(reviews); + + } + public async Task Get(int chapterId, ExternalChapterMetadataIncludes includes = ExternalChapterMetadataIncludes.ExternalReviews) + { + return await context.ExternalChapterMetadata + .Includes(includes) + .FirstOrDefaultAsync(c => c.ChapterId == chapterId); + } +} diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index c4a07dee7..32ddcf596 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -33,6 +33,7 @@ public interface IUnitOfWork IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } IEmailHistoryRepository EmailHistoryRepository { get; } + IExternalChapterMetadataRepository ExternalChapterMetadataRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); @@ -74,6 +75,7 @@ public class UnitOfWork : IUnitOfWork AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper); ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper); EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper); + ExternalChapterMetadataRepository = new ExternalChapterMetadataRepository(_context, _mapper); } /// @@ -103,6 +105,7 @@ public class UnitOfWork : IUnitOfWork public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } public IEmailHistoryRepository EmailHistoryRepository { get; } + public IExternalChapterMetadataRepository ExternalChapterMetadataRepository { get; } /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index c76658c41..3d67ccb6d 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using API.Entities.Enums; using API.Entities.Interfaces; +using API.Entities.Metadata; using API.Entities.Person; using API.Extensions; using API.Services.Tasks.Scanner.Parser; @@ -169,6 +170,8 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public Volume Volume { get; set; } = null!; public int VolumeId { get; set; } + public ExternalChapterMetadata ExternalChapterMetadata { get; set; } = null!; + public void UpdateFrom(ParserInfo info) { Files ??= new List(); diff --git a/API/Entities/Metadata/ExternalChapterMetadata.cs b/API/Entities/Metadata/ExternalChapterMetadata.cs new file mode 100644 index 000000000..a4d50896f --- /dev/null +++ b/API/Entities/Metadata/ExternalChapterMetadata.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace API.Entities.Metadata; + +/// +/// External Metadata from Kavita+ for a Chapter +/// +/// +/// As apposed to , +/// we do not have a ValidUntilUtc, as this is only matched together with the series. +/// +public class ExternalChapterMetadata +{ + public int Id { get; set; } + + public int ChapterId { get; set; } + + public ICollection ExternalReviews { get; set; } = null!; + +} diff --git a/API/Entities/Metadata/ExternalChapterReview.cs b/API/Entities/Metadata/ExternalChapterReview.cs new file mode 100644 index 000000000..81222e281 --- /dev/null +++ b/API/Entities/Metadata/ExternalChapterReview.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using API.Entities.Enums; +using API.Services.Plus; + +namespace API.Entities.Metadata; + +/// +/// Represents an Externally supplied Review for a given Series +/// +public class ExternalChapterReview +{ + public int Id { get; set; } + public string Tagline { get; set; } + public required string Body { get; set; } + /// + /// Pure text version of the body + /// + public required string BodyJustText { get; set; } + /// + /// Raw from the provider. Usually Markdown + /// + public string RawBody { get; set; } + public required ScrobbleProvider Provider { get; set; } + public RatingAuthority Authority { get; set; } = RatingAuthority.User; + public string SiteUrl { get; set; } + /// + /// Reviewer's username + /// + public string Username { get; set; } + /// + /// An Optional Rating coming from the Review + /// + public int Rating { get; set; } = 0; + /// + /// The media's overall Score + /// + public int Score { get; set; } + public int TotalVotes { get; set; } + + public int ChapterId { get; set; } + + // Relationships + public ICollection ExternalChapterMetadatas { get; set; } = null!; + +} diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index 983f6798e..8e0f48f14 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -1,6 +1,7 @@ using System.Linq; using API.Data.Repositories; using API.Entities; +using API.Entities.Metadata; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; @@ -303,4 +304,14 @@ public static class IncludesExtensions return query.AsSplitQuery(); } + + public static IQueryable Includes(this IQueryable query, ExternalChapterMetadataIncludes includeFlags) + { + if (includeFlags.HasFlag(ExternalChapterMetadataIncludes.ExternalReviews)) + { + query = query.Include(e => e.ExternalReviews); + } + + return query.AsSplitQuery(); + } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 69ed884fd..db34f2017 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -334,11 +334,19 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.IsExternal, opt => opt.MapFrom(src => true)); + CreateMap() + .ForMember(dest => dest.IsExternal, + opt => + opt.MapFrom(src => true)); CreateMap() .ForMember(dest => dest.BodyJustText, opt => opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body))); + CreateMap() + .ForMember(dest => dest.BodyJustText, + opt => + opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body))); CreateMap(); CreateMap() diff --git a/API/Helpers/Builders/RatingBuilder.cs b/API/Helpers/Builders/RatingBuilder.cs index 54af47ae8..f123f4d10 100644 --- a/API/Helpers/Builders/RatingBuilder.cs +++ b/API/Helpers/Builders/RatingBuilder.cs @@ -20,6 +20,12 @@ public class RatingBuilder : IEntityBuilder return this; } + public RatingBuilder WithChapterId(int? chapterId) + { + _rating.ChapterId = chapterId; + return this; + } + public RatingBuilder WithRating(int rating) { _rating.Rating = Math.Clamp(rating, 0, 5); diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 8350058b8..b003ce282 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1085,26 +1085,52 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification; - - madeModification = await UpdateChapterReviews(chapter, settings, potentialMatch) || madeModification; + madeModification = await UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification; _unitOfWork.ChapterRepository.Update(chapter); await _unitOfWork.CommitAsync(); } - return madeModification; } - private async Task UpdateChapterReviews(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata) + private async Task UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata) { if (!settings.Enabled) return false; - if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0) return false; + if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0) + { + _logger.LogDebug("No external reviews found for chapter {ChapterID}", chapter.Id); + return false; + } - // Clear current ratings - chapter.Ratings.Clear(); + var exteralChapterMetadata = await GetOrCreateExternalChapterMetadataForChapter(chapter.Id, chapter); + _unitOfWork.ExternalChapterMetadataRepository.Remove(exteralChapterMetadata.ExternalReviews); + List externalReviews = []; + + externalReviews.AddRange(metadata.CriticReviews + .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) + .Select(r => + { + var review = _mapper.Map(r); + review.ChapterId = chapter.Id; + review.Authority = RatingAuthority.Critic; + return review; + })); + externalReviews.AddRange(metadata.UserReviews + .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) + .Select(r => + { + var review = _mapper.Map(r); + review.ChapterId = chapter.Id; + review.Authority = RatingAuthority.User; + return review; + })); + + chapter.ExternalChapterMetadata.ExternalReviews = externalReviews; + + _logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id); return true; } @@ -1562,6 +1588,28 @@ public class ExternalMetadataService : IExternalMetadataService return externalSeriesMetadata; } + /// + /// Gets from DB or creates a new one with just ChapterId + /// + /// + /// + /// + private async Task GetOrCreateExternalChapterMetadataForChapter(int chapterId, Chapter chapter) + { + var externalChapterMetadata = await _unitOfWork.ExternalChapterMetadataRepository.Get(chapterId); + if (externalChapterMetadata != null) return externalChapterMetadata; + + externalChapterMetadata = new ExternalChapterMetadata() + { + ChapterId = chapterId, + }; + + chapter.ExternalChapterMetadata = externalChapterMetadata; + _unitOfWork.ExternalChapterMetadataRepository.Attach(externalChapterMetadata); + + return externalChapterMetadata; + } + private async Task ProcessRecommendations(LibraryType libraryType, IEnumerable recs, ExternalSeriesMetadata externalSeriesMetadata) { diff --git a/UI/Web/src/app/_models/chapter-detail.ts b/UI/Web/src/app/_models/chapter-detail.ts new file mode 100644 index 000000000..4df23767e --- /dev/null +++ b/UI/Web/src/app/_models/chapter-detail.ts @@ -0,0 +1,9 @@ +import {UserReview} from "../_single-module/review-card/user-review"; +import {Rating} from "./rating"; + +export type ChapterDetail = { + rating: number; + hasBeenRated: boolean; + reviews: UserReview[]; + ratings: Rating[]; +}; diff --git a/UI/Web/src/app/_services/chapter.service.ts b/UI/Web/src/app/_services/chapter.service.ts index 3b7aa0c43..43ecb8098 100644 --- a/UI/Web/src/app/_services/chapter.service.ts +++ b/UI/Web/src/app/_services/chapter.service.ts @@ -5,6 +5,7 @@ import {Chapter} from "../_models/chapter"; import {TextResonse} from "../_types/text-response"; import {UserReview} from "../_single-module/review-card/user-review"; import {Rating} from "../_models/rating"; +import {ChapterDetail} from "../_models/chapter-detail"; @Injectable({ providedIn: 'root' @@ -31,8 +32,8 @@ export class ChapterService { return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse); } - chapterReviews(chapterId: number) { - return this.httpClient.get>(this.baseUrl + 'chapter/review?chapterId='+chapterId); + chapterDetailPlus(seriesId: number, chapterId: number) { + return this.httpClient.get(this.baseUrl + `chapter/chapter-detail-plus?chapterId=${chapterId}&seriesId=${seriesId}`); } } diff --git a/UI/Web/src/app/_services/review.service.ts b/UI/Web/src/app/_services/review.service.ts index 5667d5dab..c13522fb6 100644 --- a/UI/Web/src/app/_services/review.service.ts +++ b/UI/Web/src/app/_services/review.service.ts @@ -13,13 +13,6 @@ export class ReviewService { 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}`); @@ -28,15 +21,15 @@ export class ReviewService { return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId); } - updateReview(seriesId: number, body: string, rating: number, chapterId?: number) { + updateReview(seriesId: number, body: string, 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, chapterId, body }); } return this.httpClient.post(this.baseUrl + 'review', { - seriesId, rating, body + seriesId, body }); } diff --git a/UI/Web/src/app/_single-module/review-card/user-review.ts b/UI/Web/src/app/_single-module/review-card/user-review.ts index fa6d4d46c..d0c631fe8 100644 --- a/UI/Web/src/app/_single-module/review-card/user-review.ts +++ b/UI/Web/src/app/_single-module/review-card/user-review.ts @@ -1,12 +1,14 @@ import {ScrobbleProvider} from "../../_services/scrobbling.service"; +export enum RatingAuthority { + User = 0, + Critic = 1 +} + export interface UserReview { seriesId: number; libraryId: number; - volumeId?: number; chapterId?: number; - rating: number; - hasBeenRated: boolean; score: number; username: string; body: string; @@ -15,4 +17,5 @@ export interface UserReview { bodyJustText?: string; siteUrl?: string; provider: ScrobbleProvider; + authority: RatingAuthority; } 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 c9f2eb6bf..2470f1679 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 @@ -35,31 +35,21 @@ export class ReviewModalComponent implements OnInit { protected readonly modal = inject(NgbActiveModal); private readonly reviewService = inject(ReviewService); - private readonly seriesService = inject(SeriesService); private readonly cdRef = inject(ChangeDetectorRef); private readonly confirmService = inject(ConfirmService); private readonly toastr = inject(ToastrService); - private readonly themeService = inject(ThemeService); protected readonly minLength = 5; @Input({required: true}) review!: UserReview; reviewGroup!: FormGroup; - rating: number = 0; - - starColor = this.themeService.getCssVariable('--rating-star-color'); ngOnInit(): void { this.reviewGroup = new FormGroup({ reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]), }); - this.rating = this.review.rating; this.cdRef.markForCheck(); } - updateRating($event: number) { - this.rating = $event; - } - close() { this.modal.close({success: false, review: this.review, action: ReviewModalCloseAction.Close}); } @@ -79,7 +69,7 @@ export class ReviewModalComponent implements OnInit { return; } - this.reviewService.updateReview(this.review.seriesId, model.reviewBody, this.rating, this.review.chapterId).subscribe(review => { + this.reviewService.updateReview(this.review.seriesId, model.reviewBody, this.review.chapterId).subscribe(review => { this.modal.close({success: true, review: review, action: ReviewModalCloseAction.Edit}); }); diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.html b/UI/Web/src/app/chapter-detail/chapter-detail.component.html index 8afe2ffc3..ffb69cc67 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.html +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.html @@ -28,11 +28,10 @@
- @let rating = userRating(); diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts index 081cf2a7a..3c4b9806f 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts @@ -73,10 +73,11 @@ import {ReviewModalComponent} from "../_single-module/review-modal/review-modal. import {ReviewsComponent} from "../_single-module/reviews/reviews.component"; import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component"; import {Rating} from "../_models/rating"; +import {ReviewService} from "../_services/review.service"; enum TabID { Related = 'related-tab', - Reviews = 'review-tab', // Only applicable for books + Reviews = 'review-tab', Details = 'details-tab' } @@ -111,8 +112,6 @@ enum TabID { DatePipe, DefaultDatePipe, CoverImageComponent, - CarouselReelComponent, - ReviewCardComponent, ReviewsComponent, ExternalRatingComponent ], @@ -165,6 +164,9 @@ export class ChapterDetailComponent implements OnInit { hasReadingProgress = false; userReviews: Array = []; plusReviews: Array = []; + rating: number = 0; + hasBeenRated: boolean = false; + weblinks: Array = []; activeTabId = TabID.Details; /** @@ -233,7 +235,7 @@ export class ChapterDetailComponent implements OnInit { series: this.seriesService.getSeries(this.seriesId), chapter: this.chapterService.getChapterMetadata(this.chapterId), libraryType: this.libraryService.getLibraryType(this.libraryId), - reviews: this.chapterService.chapterReviews(this.chapterId), + chapterDetail: this.chapterService.chapterDetailPlus(this.seriesId, this.chapterId), }).subscribe(results => { if (results.chapter === null) { @@ -245,8 +247,10 @@ export class ChapterDetailComponent implements OnInit { this.chapter = results.chapter; this.weblinks = this.chapter.webLinks.split(','); this.libraryType = results.libraryType; - this.userReviews = results.reviews.filter(r => !r.isExternal); - this.plusReviews = results.reviews.filter(r => r.isExternal); + this.userReviews = results.chapterDetail.reviews.filter(r => !r.isExternal); + this.plusReviews = results.chapterDetail.reviews.filter(r => r.isExternal); + this.rating = results.chapterDetail.rating; + this.hasBeenRated = results.chapterDetail.hasBeenRated; this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor); @@ -386,10 +390,6 @@ export class ChapterDetailComponent implements OnInit { } } - userRating() { - return this.userReviews.find(r => r.username == this.user?.username && !r.isExternal) - } - protected readonly LibraryType = LibraryType; protected readonly encodeURIComponent = encodeURIComponent; } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index 00fee7fdd..3e00f89aa 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -31,11 +31,10 @@ @if (libraryType !== null && series && volume.chapters.length === 1) {
- @let rating = userRating(); 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 0c573b1ea..4ea972018 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -83,6 +83,7 @@ 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"; +import {ChapterService} from "../_services/chapter.service"; enum TabID { @@ -183,7 +184,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); + private readonly chapterService = inject(ChapterService); protected readonly AgeRating = AgeRating; @@ -204,8 +205,13 @@ export class VolumeDetailComponent implements OnInit { libraryType: LibraryType | null = null; activeTabId = TabID.Chapters; readingLists: ReadingList[] = []; + + // Only populated if the volume has exactly one chapter userReviews: Array = []; plusReviews: Array = []; + rating: number = 0; + hasBeenRated: boolean = false; + mobileSeriesImgBackground: string | undefined; downloadInProgress: boolean = false; @@ -405,9 +411,11 @@ export class VolumeDetailComponent implements OnInit { this.libraryType = results.libraryType; 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.chapterService.chapterDetailPlus(this.seriesId, this.volume.chapters[0].id).subscribe(detail => { + this.userReviews = detail.reviews.filter(r => !r.isExternal); + this.plusReviews = detail.reviews.filter(r => r.isExternal); + this.rating = detail.rating; + this.hasBeenRated = detail.hasBeenRated; }); } @@ -692,9 +700,5 @@ export class VolumeDetailComponent implements OnInit { } } - userRating() { - return this.userReviews.find(r => r.username === this.user?.username && !r.isExternal); - } - protected readonly encodeURIComponent = encodeURIComponent; }