Simply entities & seperate endpoints

This commit is contained in:
Amelia 2025-04-28 20:31:47 +02:00
parent 41faa30e6f
commit 6d4dfcda67
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
22 changed files with 299 additions and 615 deletions

View file

@ -935,7 +935,7 @@ public class SeriesFilterTests : AbstractDbTest
var zeroRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2); var zeroRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2);
Assert.NotNull(zeroRating); Assert.NotNull(zeroRating);
Assert.True(await ratingService.UpdateRating(user, new UpdateRatingDto() Assert.True(await ratingService.UpdateSeriesRating(user, new UpdateRatingDto()
{ {
SeriesId = zeroRating.Id, SeriesId = zeroRating.Id,
UserRating = 0 UserRating = 0
@ -944,7 +944,7 @@ public class SeriesFilterTests : AbstractDbTest
// Select 4.5 Rating // Select 4.5 Rating
var partialRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(3); var partialRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(3);
Assert.True(await ratingService.UpdateRating(user, new UpdateRatingDto() Assert.True(await ratingService.UpdateSeriesRating(user, new UpdateRatingDto()
{ {
SeriesId = partialRating.Id, SeriesId = partialRating.Id,
UserRating = 4.5f UserRating = 4.5f

View file

@ -45,7 +45,7 @@ public class RatingServiceTests: AbstractDbTest
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings);
JobStorage.Current = new InMemoryStorage(); JobStorage.Current = new InMemoryStorage();
var result = await _ratingService.UpdateRating(user, new UpdateRatingDto var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto
{ {
SeriesId = 1, SeriesId = 1,
UserRating = 3, UserRating = 3,
@ -79,7 +79,7 @@ public class RatingServiceTests: AbstractDbTest
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings);
var result = await _ratingService.UpdateRating(user, new UpdateRatingDto var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto
{ {
SeriesId = 1, SeriesId = 1,
UserRating = 3, UserRating = 3,
@ -95,7 +95,7 @@ public class RatingServiceTests: AbstractDbTest
// Update the DB again // Update the DB again
var result2 = await _ratingService.UpdateRating(user, new UpdateRatingDto var result2 = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto
{ {
SeriesId = 1, SeriesId = 1,
UserRating = 5, UserRating = 5,
@ -129,7 +129,7 @@ public class RatingServiceTests: AbstractDbTest
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings);
var result = await _ratingService.UpdateRating(user, new UpdateRatingDto var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto
{ {
SeriesId = 1, SeriesId = 1,
UserRating = 10, UserRating = 10,
@ -164,7 +164,7 @@ public class RatingServiceTests: AbstractDbTest
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings);
var result = await _ratingService.UpdateRating(user, new UpdateRatingDto var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto
{ {
SeriesId = 2, SeriesId = 2,
UserRating = 5, UserRating = 5,

View file

@ -399,7 +399,7 @@ public class ChapterController : BaseApiController
[HttpGet("chapter-detail-plus")] [HttpGet("chapter-detail-plus")]
public async Task<ChapterDetailPlusDto> ChapterDetailPlus([FromQuery] int chapterId) public async Task<ActionResult<ChapterDetailPlusDto>> ChapterDetailPlus([FromQuery] int chapterId)
{ {
var ret = new ChapterDetailPlusDto(); var ret = new ChapterDetailPlusDto();
@ -415,11 +415,10 @@ public class ChapterController : BaseApiController
ret.HasBeenRated = ownRating.HasBeenRated; ret.HasBeenRated = ownRating.HasBeenRated;
} }
var externalMetadata = await _unitOfWork.ExternalChapterMetadataRepository.Get(chapterId); var externalReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReviews(chapterId);
if (externalMetadata != null && externalMetadata.ExternalReviews.Count > 0) if (externalReviews.Count > 0)
{ {
var dtos = externalMetadata.ExternalReviews.Select(ex => _mapper.Map<UserReviewDto>(ex)).ToList(); userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(externalReviews));
userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(dtos));
} }
ret.Reviews = userReviews; ret.Reviews = userReviews;

View file

@ -1,17 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.Extensions; using API.Extensions;
using API.Services; using API.Services;
using API.Services.Plus; using API.Services.Plus;
using EasyCaching.Core;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers; namespace API.Controllers;
@ -33,13 +28,19 @@ public class RatingController : BaseApiController
_localizationService = localizationService; _localizationService = localizationService;
} }
[HttpPost] /// <summary>
public async Task<ActionResult> UpdateRating(UpdateRatingDto updateRating) /// Update the users' rating of the given series
/// </summary>
/// <param name="updateRating"></param>
/// <returns></returns>
/// <exception cref="UnauthorizedAccessException"></exception>
[HttpPost("series")]
public async Task<ActionResult> UpdateSeriesRating(UpdateRatingDto updateRating)
{ {
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings);
if (user == null) throw new UnauthorizedAccessException(); if (user == null) throw new UnauthorizedAccessException();
if (await _ratingService.UpdateRating(user, updateRating)) if (await _ratingService.UpdateSeriesRating(user, updateRating))
{ {
return Ok(); return Ok();
} }
@ -47,24 +48,44 @@ public class RatingController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
} }
[HttpGet("overall")] /// <summary>
public async Task<ActionResult<RatingDto>> GetOverallRating(int seriesId, [FromQuery] int? chapterId) /// Update the users' rating of the given chapter
/// </summary>
/// <param name="updateRating">chapterId must be set</param>
/// <returns></returns>
/// <exception cref="UnauthorizedAccessException"></exception>
[HttpPost("chapter")]
public async Task<ActionResult> UpdateChapterRating(UpdateRatingDto updateRating)
{ {
int average; var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings);
if (chapterId != null) if (user == null) throw new UnauthorizedAccessException();
if (await _ratingService.UpdateChapterRating(user, updateRating))
{ {
average = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId.Value, User.GetUserId()); return Ok();
}
else
{
average = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId());
} }
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
}
[HttpGet("overall-series")]
public async Task<ActionResult<RatingDto>> GetOverallSeriesRating(int seriesId)
{
return Ok(new RatingDto() return Ok(new RatingDto()
{ {
Provider = ScrobbleProvider.Kavita, Provider = ScrobbleProvider.Kavita,
AverageScore = average, AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()),
FavoriteCount = 0,
});
}
[HttpGet("overall-chapter")]
public async Task<ActionResult<RatingDto>> GetOverallChapterRating(int chapterId)
{
return Ok(new RatingDto()
{
Provider = ScrobbleProvider.Kavita,
AverageScore = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, User.GetUserId()),
FavoriteCount = 0, FavoriteCount = 0,
}); });
} }

View file

@ -33,39 +33,16 @@ public class ReviewController : BaseApiController
/// <summary> /// <summary>
/// Updates the review for a given series, or chapter /// Updates the user's review for a given series
/// </summary> /// </summary>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost] [HttpPost("series")]
public async Task<ActionResult<UserReviewDto>> UpdateReview(UpdateUserReviewDto dto) public async Task<ActionResult<UserReviewDto>> UpdateSeriesReview(UpdateUserReviewDto dto)
{ {
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings);
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
UserReviewDto review;
if (dto.ChapterId != null)
{
review = await UpdateChapterReview(user, dto, dto.ChapterId.Value);
}
else
{
review = await UpdateSeriesReview(user, dto);
}
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
if (dto.ChapterId == null)
{
BackgroundJob.Enqueue(() =>
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
}
return Ok(review);
}
private async Task<UserReviewDto> UpdateSeriesReview(AppUser user, UpdateUserReviewDto dto)
{
var ratingBuilder = new RatingBuilder(await _unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); var ratingBuilder = new RatingBuilder(await _unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id));
var rating = ratingBuilder var rating = ratingBuilder
@ -78,11 +55,31 @@ public class ReviewController : BaseApiController
{ {
user.Ratings.Add(rating); user.Ratings.Add(rating);
} }
return _mapper.Map<UserReviewDto>(rating);
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
BackgroundJob.Enqueue(() =>
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
return Ok(_mapper.Map<UserReviewDto>(rating));
} }
private async Task<UserReviewDto> UpdateChapterReview(AppUser user, UpdateUserReviewDto dto, int chapterId) /// <summary>
/// Update the user's review for a given chapter
/// </summary>
/// <param name="dto">chapterId must be set</param>
/// <returns></returns>
[HttpPost("chapter")]
public async Task<ActionResult<UserReviewDto>> UpdateChapterReview(UpdateUserReviewDto dto)
{ {
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings);
if (user == null) return Unauthorized();
if (dto.ChapterId == null) return BadRequest();
int chapterId = dto.ChapterId.Value;
var ratingBuilder = new ChapterRatingBuilder(await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); var ratingBuilder = new ChapterRatingBuilder(await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId));
var rating = ratingBuilder var rating = ratingBuilder
@ -95,29 +92,46 @@ public class ReviewController : BaseApiController
{ {
user.ChapterRatings.Add(rating); user.ChapterRatings.Add(rating);
} }
return _mapper.Map<UserReviewDto>(rating);
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
return Ok(_mapper.Map<UserReviewDto>(rating));
} }
/// <summary> /// <summary>
/// Deletes the user's review for the given series, or chapter /// Deletes the user's review for the given series
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[HttpDelete] [HttpDelete("series")]
public async Task<ActionResult> DeleteReview([FromQuery] int seriesId, [FromQuery] int? chapterId) public async Task<ActionResult> DeleteSeriesReview([FromQuery] int seriesId)
{ {
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings);
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
if (chapterId != null)
{
user.ChapterRatings = user.ChapterRatings.Where(r => r.ChapterId != chapterId).ToList();
}
else
{
user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList();
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
return Ok();
} }
/// <summary>
/// Deletes the user's review for the given chapter
/// </summary>
/// <returns></returns>
[HttpDelete("chapter")]
public async Task<ActionResult> DeleteChapterReview([FromQuery] int chapterId)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings);
if (user == null) return Unauthorized();
user.ChapterRatings = user.ChapterRatings.Where(r => r.ChapterId != chapterId).ToList();
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();

View file

@ -79,8 +79,6 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<MetadataSettings> MetadataSettings { get; set; } = null!; public DbSet<MetadataSettings> MetadataSettings { get; set; } = null!;
public DbSet<MetadataFieldMapping> MetadataFieldMapping { get; set; } = null!; public DbSet<MetadataFieldMapping> MetadataFieldMapping { get; set; } = null!;
public DbSet<AppUserChapterRating> AppUserChapterRating { get; set; } = null!; public DbSet<AppUserChapterRating> AppUserChapterRating { get; set; } = null!;
public DbSet<ExternalChapterReview> ExternalChapterReview { get; set; } = null!;
public DbSet<ExternalChapterMetadata> ExternalChapterMetadata { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {

View file

@ -1,159 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterRating : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppUserChapterRating",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Rating = table.Column<float>(type: "REAL", nullable: false),
HasBeenRated = table.Column<bool>(type: "INTEGER", nullable: false),
Review = table.Column<string>(type: "TEXT", nullable: true),
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
AppUserId = table.Column<int>(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);
});
migrationBuilder.CreateTable(
name: "ExternalChapterMetadata",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ChapterId = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Tagline = table.Column<string>(type: "TEXT", nullable: true),
Body = table.Column<string>(type: "TEXT", nullable: true),
BodyJustText = table.Column<string>(type: "TEXT", nullable: true),
RawBody = table.Column<string>(type: "TEXT", nullable: true),
Provider = table.Column<int>(type: "INTEGER", nullable: false),
Authority = table.Column<int>(type: "INTEGER", nullable: false),
SiteUrl = table.Column<string>(type: "TEXT", nullable: true),
Username = table.Column<string>(type: "TEXT", nullable: true),
Rating = table.Column<int>(type: "INTEGER", nullable: false),
Score = table.Column<int>(type: "INTEGER", nullable: false),
TotalVotes = table.Column<int>(type: "INTEGER", nullable: false),
ChapterId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ExternalChapterReview", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ExternalChapterMetadataExternalChapterReview",
columns: table => new
{
ExternalChapterMetadatasId = table.Column<int>(type: "INTEGER", nullable: false),
ExternalReviewsId = table.Column<int>(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_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_ExternalChapterMetadata_ChapterId",
table: "ExternalChapterMetadata",
column: "ChapterId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ExternalChapterMetadataExternalChapterReview_ExternalReviewsId",
table: "ExternalChapterMetadataExternalChapterReview",
column: "ExternalReviewsId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppUserChapterRating");
migrationBuilder.DropTable(
name: "ExternalChapterMetadataExternalChapterReview");
migrationBuilder.DropTable(
name: "ExternalChapterMetadata");
migrationBuilder.DropTable(
name: "ExternalChapterReview");
}
}
}

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations namespace API.Data.Migrations
{ {
[DbContext(typeof(DataContext))] [DbContext(typeof(DataContext))]
[Migration("20250428151027_ChapterRating")] [Migration("20250428180534_ChapterRating")]
partial class ChapterRating partial class ChapterRating
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1348,70 +1348,6 @@ namespace API.Data.Migrations
b.ToTable("MediaError"); b.ToTable("MediaError");
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId")
.IsUnique();
b.ToTable("ExternalChapterMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterReview", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<string>("Body")
.HasColumnType("TEXT");
b.Property<string>("BodyJustText")
.HasColumnType("TEXT");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Provider")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("RawBody")
.HasColumnType("TEXT");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.Property<string>("SiteUrl")
.HasColumnType("TEXT");
b.Property<string>("Tagline")
.HasColumnType("TEXT");
b.Property<int>("TotalVotes")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ExternalChapterReview");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1481,12 +1417,18 @@ namespace API.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<string>("Body") b.Property<string>("Body")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("BodyJustText") b.Property<string>("BodyJustText")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Provider") b.Property<int>("Provider")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1516,6 +1458,8 @@ namespace API.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ChapterId");
b.ToTable("ExternalReview"); b.ToTable("ExternalReview");
}); });
@ -2550,21 +2494,6 @@ namespace API.Data.Migrations
b.ToTable("CollectionTagSeriesMetadata"); b.ToTable("CollectionTagSeriesMetadata");
}); });
modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b =>
{
b.Property<int>("ExternalChapterMetadatasId")
.HasColumnType("INTEGER");
b.Property<int>("ExternalReviewsId")
.HasColumnType("INTEGER");
b.HasKey("ExternalChapterMetadatasId", "ExternalReviewsId");
b.HasIndex("ExternalReviewsId");
b.ToTable("ExternalChapterMetadataExternalChapterReview");
});
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{ {
b.Property<int>("ExternalRatingsId") b.Property<int>("ExternalRatingsId")
@ -3049,13 +2978,11 @@ namespace API.Data.Migrations
b.Navigation("Chapter"); b.Navigation("Chapter");
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b => modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
{ {
b.HasOne("API.Entities.Chapter", null) b.HasOne("API.Entities.Chapter", null)
.WithOne("ExternalChapterMetadata") .WithMany("ExternalReviews")
.HasForeignKey("API.Entities.Metadata.ExternalChapterMetadata", "ChapterId") .HasForeignKey("ChapterId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
@ -3365,21 +3292,6 @@ namespace API.Data.Migrations
.IsRequired(); .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 => modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{ {
b.HasOne("API.Entities.Metadata.ExternalRating", null) b.HasOne("API.Entities.Metadata.ExternalRating", null)
@ -3533,7 +3445,7 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b => modelBuilder.Entity("API.Entities.Chapter", b =>
{ {
b.Navigation("ExternalChapterMetadata"); b.Navigation("ExternalReviews");
b.Navigation("Files"); b.Navigation("Files");

View file

@ -0,0 +1,113 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterRating : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Authority",
table: "ExternalReview",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "ChapterId",
table: "ExternalReview",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateTable(
name: "AppUserChapterRating",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Rating = table.Column<float>(type: "REAL", nullable: false),
HasBeenRated = table.Column<bool>(type: "INTEGER", nullable: false),
Review = table.Column<string>(type: "TEXT", nullable: true),
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
AppUserId = table.Column<int>(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);
});
migrationBuilder.CreateIndex(
name: "IX_ExternalReview_ChapterId",
table: "ExternalReview",
column: "ChapterId");
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.AddForeignKey(
name: "FK_ExternalReview_Chapter_ChapterId",
table: "ExternalReview",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ExternalReview_Chapter_ChapterId",
table: "ExternalReview");
migrationBuilder.DropTable(
name: "AppUserChapterRating");
migrationBuilder.DropIndex(
name: "IX_ExternalReview_ChapterId",
table: "ExternalReview");
migrationBuilder.DropColumn(
name: "Authority",
table: "ExternalReview");
migrationBuilder.DropColumn(
name: "ChapterId",
table: "ExternalReview");
}
}
}

View file

@ -1345,70 +1345,6 @@ namespace API.Data.Migrations
b.ToTable("MediaError"); b.ToTable("MediaError");
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId")
.IsUnique();
b.ToTable("ExternalChapterMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterReview", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<string>("Body")
.HasColumnType("TEXT");
b.Property<string>("BodyJustText")
.HasColumnType("TEXT");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Provider")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("RawBody")
.HasColumnType("TEXT");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.Property<string>("SiteUrl")
.HasColumnType("TEXT");
b.Property<string>("Tagline")
.HasColumnType("TEXT");
b.Property<int>("TotalVotes")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ExternalChapterReview");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1478,12 +1414,18 @@ namespace API.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<string>("Body") b.Property<string>("Body")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("BodyJustText") b.Property<string>("BodyJustText")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Provider") b.Property<int>("Provider")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1513,6 +1455,8 @@ namespace API.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ChapterId");
b.ToTable("ExternalReview"); b.ToTable("ExternalReview");
}); });
@ -2547,21 +2491,6 @@ namespace API.Data.Migrations
b.ToTable("CollectionTagSeriesMetadata"); b.ToTable("CollectionTagSeriesMetadata");
}); });
modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b =>
{
b.Property<int>("ExternalChapterMetadatasId")
.HasColumnType("INTEGER");
b.Property<int>("ExternalReviewsId")
.HasColumnType("INTEGER");
b.HasKey("ExternalChapterMetadatasId", "ExternalReviewsId");
b.HasIndex("ExternalReviewsId");
b.ToTable("ExternalChapterMetadataExternalChapterReview");
});
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{ {
b.Property<int>("ExternalRatingsId") b.Property<int>("ExternalRatingsId")
@ -3046,13 +2975,11 @@ namespace API.Data.Migrations
b.Navigation("Chapter"); b.Navigation("Chapter");
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b => modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
{ {
b.HasOne("API.Entities.Chapter", null) b.HasOne("API.Entities.Chapter", null)
.WithOne("ExternalChapterMetadata") .WithMany("ExternalReviews")
.HasForeignKey("API.Entities.Metadata.ExternalChapterMetadata", "ChapterId") .HasForeignKey("ChapterId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
@ -3362,21 +3289,6 @@ namespace API.Data.Migrations
.IsRequired(); .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 => modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{ {
b.HasOne("API.Entities.Metadata.ExternalRating", null) b.HasOne("API.Entities.Metadata.ExternalRating", null)
@ -3530,7 +3442,7 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b => modelBuilder.Entity("API.Entities.Chapter", b =>
{ {
b.Navigation("ExternalChapterMetadata"); b.Navigation("ExternalReviews");
b.Navigation("Files"); b.Navigation("Files");

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.DTOs.Reader; using API.DTOs.Reader;
using API.DTOs.SeriesDetail;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
@ -24,7 +25,8 @@ public enum ChapterIncludes
Files = 4, Files = 4,
People = 8, People = 8,
Genres = 16, Genres = 16,
Tags = 32 Tags = 32,
ExternalReviews = 1 << 6,
} }
public interface IChapterRepository public interface IChapterRepository
@ -49,6 +51,7 @@ public interface IChapterRepository
IEnumerable<Chapter> GetChaptersForSeries(int seriesId); IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId); Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
Task<int> GetAverageUserRating(int chapterId, int userId); Task<int> GetAverageUserRating(int chapterId, int userId);
Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId);
} }
public class ChapterRepository : IChapterRepository public class ChapterRepository : IChapterRepository
{ {
@ -327,4 +330,14 @@ public class ChapterRepository : IChapterRepository
.AverageAsync(r => (int?) r.Rating)); .AverageAsync(r => (int?) r.Rating));
return avg.HasValue ? (int) (avg.Value * 20) : 0; return avg.HasValue ? (int) (avg.Value * 20) : 0;
} }
public async Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId)
{
return await _context.Chapter
.Where(c => c.Id == chapterId)
.SelectMany(c => c.ExternalReviews)
// Don't use ProjectTo, it fails to map int to float (??)
.Select(r => _mapper.Map<UserReviewDto>(r))
.ToListAsync();
}
} }

View file

@ -1,44 +0,0 @@
#nullable enable
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<ExternalChapterReview>? reviews);
Task<ExternalChapterMetadata?> 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<ExternalChapterReview>? reviews)
{
if (reviews == null) return;
context.ExternalChapterReview.RemoveRange(reviews);
}
public async Task<ExternalChapterMetadata?> Get(int chapterId, ExternalChapterMetadataIncludes includes = ExternalChapterMetadataIncludes.ExternalReviews)
{
return await context.ExternalChapterMetadata
.Includes(includes)
.FirstOrDefaultAsync(c => c.ChapterId == chapterId);
}
}

View file

@ -33,7 +33,6 @@ public interface IUnitOfWork
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
IEmailHistoryRepository EmailHistoryRepository { get; } IEmailHistoryRepository EmailHistoryRepository { get; }
IExternalChapterMetadataRepository ExternalChapterMetadataRepository { get; }
bool Commit(); bool Commit();
Task<bool> CommitAsync(); Task<bool> CommitAsync();
bool HasChanges(); bool HasChanges();
@ -75,7 +74,6 @@ public class UnitOfWork : IUnitOfWork
AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper); AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper);
ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper); ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper);
EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper); EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper);
ExternalChapterMetadataRepository = new ExternalChapterMetadataRepository(_context, _mapper);
} }
/// <summary> /// <summary>
@ -105,7 +103,6 @@ public class UnitOfWork : IUnitOfWork
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
public IEmailHistoryRepository EmailHistoryRepository { get; } public IEmailHistoryRepository EmailHistoryRepository { get; }
public IExternalChapterMetadataRepository ExternalChapterMetadataRepository { get; }
/// <summary> /// <summary>
/// Commits changes to the DB. Completes the open transaction. /// Commits changes to the DB. Completes the open transaction.

View file

@ -170,7 +170,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
public Volume Volume { get; set; } = null!; public Volume Volume { get; set; } = null!;
public int VolumeId { get; set; } public int VolumeId { get; set; }
public ExternalChapterMetadata ExternalChapterMetadata { get; set; } = null!; public ICollection<ExternalReview> ExternalReviews { get; set; } = [];
public void UpdateFrom(ParserInfo info) public void UpdateFrom(ParserInfo info)
{ {

View file

@ -1,20 +0,0 @@
using System.Collections.Generic;
namespace API.Entities.Metadata;
/// <summary>
/// External Metadata from Kavita+ for a Chapter
/// </summary>
/// <remarks>
/// As apposed to <see cref="ExternalSeriesMetadata"/>,
/// we do not have a ValidUntilUtc, as this is only matched together with the series.
/// </remarks>
public class ExternalChapterMetadata
{
public int Id { get; set; }
public int ChapterId { get; set; }
public ICollection<ExternalChapterReview> ExternalReviews { get; set; } = null!;
}

View file

@ -1,45 +0,0 @@
using System.Collections.Generic;
using API.Entities.Enums;
using API.Services.Plus;
namespace API.Entities.Metadata;
/// <summary>
/// Represents an Externally supplied Review for a given Series
/// </summary>
public class ExternalChapterReview
{
public int Id { get; set; }
public string Tagline { get; set; }
public required string Body { get; set; }
/// <summary>
/// Pure text version of the body
/// </summary>
public required string BodyJustText { get; set; }
/// <summary>
/// Raw from the provider. Usually Markdown
/// </summary>
public string RawBody { get; set; }
public required ScrobbleProvider Provider { get; set; }
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
public string SiteUrl { get; set; }
/// <summary>
/// Reviewer's username
/// </summary>
public string Username { get; set; }
/// <summary>
/// An Optional Rating coming from the Review
/// </summary>
public int Rating { get; set; } = 0;
/// <summary>
/// The media's overall Score
/// </summary>
public int Score { get; set; }
public int TotalVotes { get; set; }
public int ChapterId { get; set; }
// Relationships
public ICollection<ExternalChapterMetadata> ExternalChapterMetadatas { get; set; } = null!;
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities.Enums;
using API.Services.Plus; using API.Services.Plus;
namespace API.Entities.Metadata; namespace API.Entities.Metadata;
@ -20,6 +21,7 @@ public class ExternalReview
/// </summary> /// </summary>
public string RawBody { get; set; } public string RawBody { get; set; }
public required ScrobbleProvider Provider { get; set; } public required ScrobbleProvider Provider { get; set; }
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
public string SiteUrl { get; set; } public string SiteUrl { get; set; }
/// <summary> /// <summary>
/// Reviewer's username /// Reviewer's username
@ -37,6 +39,7 @@ public class ExternalReview
public int SeriesId { get; set; } public int SeriesId { get; set; }
public int? ChapterId { get; set; }
// Relationships // Relationships
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!; public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;

View file

@ -73,6 +73,12 @@ public static class IncludesExtensions
.Include(c => c.Tags); .Include(c => c.Tags);
} }
if (includes.HasFlag(ChapterIncludes.ExternalReviews))
{
queryable = queryable
.Include(c => c.ExternalReviews);
}
return queryable.AsSplitQuery(); return queryable.AsSplitQuery();
} }
@ -309,14 +315,4 @@ public static class IncludesExtensions
return query.AsSplitQuery(); return query.AsSplitQuery();
} }
public static IQueryable<ExternalChapterMetadata> Includes(this IQueryable<ExternalChapterMetadata> query, ExternalChapterMetadataIncludes includeFlags)
{
if (includeFlags.HasFlag(ExternalChapterMetadataIncludes.ExternalReviews))
{
query = query.Include(e => e.ExternalReviews);
}
return query.AsSplitQuery();
}
} }

View file

@ -344,19 +344,11 @@ public class AutoMapperProfiles : Profile
.ForMember(dest => dest.IsExternal, .ForMember(dest => dest.IsExternal,
opt => opt =>
opt.MapFrom(src => true)); opt.MapFrom(src => true));
CreateMap<ExternalChapterReview, UserReviewDto>()
.ForMember(dest => dest.IsExternal,
opt =>
opt.MapFrom(src => true));
CreateMap<UserReviewDto, ExternalReview>() CreateMap<UserReviewDto, ExternalReview>()
.ForMember(dest => dest.BodyJustText, .ForMember(dest => dest.BodyJustText,
opt => opt =>
opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body))); opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body)));
CreateMap<UserReviewDto, ExternalChapterReview>()
.ForMember(dest => dest.BodyJustText,
opt =>
opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body)));
CreateMap<ExternalRecommendation, ExternalSeriesDto>(); CreateMap<ExternalRecommendation, ExternalSeriesDto>();
CreateMap<Series, ManageMatchSeriesDto>() CreateMap<Series, ManageMatchSeriesDto>()

View file

@ -1104,16 +1104,15 @@ public class ExternalMetadataService : IExternalMetadataService
return false; return false;
} }
var exteralChapterMetadata = await GetOrCreateExternalChapterMetadataForChapter(chapter.Id, chapter); _unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews);
_unitOfWork.ExternalChapterMetadataRepository.Remove(exteralChapterMetadata.ExternalReviews);
List<ExternalChapterReview> externalReviews = []; List<ExternalReview> externalReviews = [];
externalReviews.AddRange(metadata.CriticReviews externalReviews.AddRange(metadata.CriticReviews
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
.Select(r => .Select(r =>
{ {
var review = _mapper.Map<ExternalChapterReview>(r); var review = _mapper.Map<ExternalReview>(r);
review.ChapterId = chapter.Id; review.ChapterId = chapter.Id;
review.Authority = RatingAuthority.Critic; review.Authority = RatingAuthority.Critic;
return review; return review;
@ -1122,13 +1121,13 @@ public class ExternalMetadataService : IExternalMetadataService
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
.Select(r => .Select(r =>
{ {
var review = _mapper.Map<ExternalChapterReview>(r); var review = _mapper.Map<ExternalReview>(r);
review.ChapterId = chapter.Id; review.ChapterId = chapter.Id;
review.Authority = RatingAuthority.User; review.Authority = RatingAuthority.User;
return review; return review;
})); }));
chapter.ExternalChapterMetadata.ExternalReviews = externalReviews; chapter.ExternalReviews = externalReviews;
_logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id); _logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id);
return true; return true;
@ -1588,28 +1587,6 @@ public class ExternalMetadataService : IExternalMetadataService
return externalSeriesMetadata; return externalSeriesMetadata;
} }
/// <summary>
/// Gets from DB or creates a new one with just ChapterId
/// </summary>
/// <param name="chapterId"></param>
/// <param name="chapter"></param>
/// <returns></returns>
private async Task<ExternalChapterMetadata> 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<RecommendationDto> ProcessRecommendations(LibraryType libraryType, IEnumerable<MediaRecommendationDto> recs, private async Task<RecommendationDto> ProcessRecommendations(LibraryType libraryType, IEnumerable<MediaRecommendationDto> recs,
ExternalSeriesMetadata externalSeriesMetadata) ExternalSeriesMetadata externalSeriesMetadata)
{ {

View file

@ -14,12 +14,20 @@ namespace API.Services;
public interface IRatingService public interface IRatingService
{ {
/// <summary> /// <summary>
/// /// Updates the users' rating for a given series
/// </summary> /// </summary>
/// <param name="user">Should include ratings</param> /// <param name="user">Should include ratings</param>
/// <param name="updateRatingDto"></param> /// <param name="updateRatingDto"></param>
/// <returns></returns> /// <returns></returns>
Task<bool> UpdateRating(AppUser user, UpdateRatingDto updateRatingDto); Task<bool> UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto);
/// <summary>
/// Updates the users' rating for a given chapter
/// </summary>
/// <param name="user">Should include ratings</param>
/// <param name="updateRatingDto">chapterId must be set</param>
/// <returns></returns>
Task<bool> UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto);
} }
public class RatingService: IRatingService public class RatingService: IRatingService
@ -36,17 +44,7 @@ public class RatingService: IRatingService
_logger = logger; _logger = logger;
} }
public async Task<bool> UpdateRating(AppUser user, UpdateRatingDto updateRatingDto) public async Task<bool> UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto)
{
if (updateRatingDto.ChapterId != null)
{
return await UpdateChapterRating(user, updateRatingDto);
}
return await UpdateSeriesRating(user, updateRatingDto);
}
private async Task<bool> UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto)
{ {
var userRating = var userRating =
await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id) ?? await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id) ??
@ -85,7 +83,7 @@ public class RatingService: IRatingService
return false; return false;
} }
private async Task<bool> UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto) public async Task<bool> UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto)
{ {
if (updateRatingDto.ChapterId == null) if (updateRatingDto.ChapterId == null)
{ {

View file

@ -15,35 +15,42 @@ export class ReviewService {
deleteReview(seriesId: number, chapterId?: number) { deleteReview(seriesId: number, chapterId?: number) {
if (chapterId) { if (chapterId) {
return this.httpClient.delete(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`); return this.httpClient.delete(this.baseUrl + `review/chapter?chapterId=${chapterId}`);
} }
return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId); return this.httpClient.delete(this.baseUrl + `review/series?seriesId=${seriesId}`);
} }
updateReview(seriesId: number, body: string, chapterId?: number) { updateReview(seriesId: number, body: string, chapterId?: number) {
if (chapterId) { if (chapterId) {
return this.httpClient.post<UserReview>(this.baseUrl + `review`, { return this.httpClient.post<UserReview>(this.baseUrl + `review/chapter`, {
seriesId, chapterId, body seriesId, chapterId, body
}); });
} }
return this.httpClient.post<UserReview>(this.baseUrl + 'review', { return this.httpClient.post<UserReview>(this.baseUrl + 'review/series', {
seriesId, body seriesId, body
}); });
} }
updateRating(seriesId: number, userRating: number, chapterId?: number) { updateRating(seriesId: number, userRating: number, chapterId?: number) {
return this.httpClient.post(this.baseUrl + 'rating', { if (chapterId) {
return this.httpClient.post(this.baseUrl + 'rating/chapter', {
seriesId, chapterId, userRating seriesId, chapterId, userRating
}) })
} }
return this.httpClient.post(this.baseUrl + 'rating/series', {
seriesId, userRating
})
}
overallRating(seriesId: number, chapterId?: number) { overallRating(seriesId: number, chapterId?: number) {
if (chapterId) { if (chapterId) {
return this.httpClient.get<Rating>(this.baseUrl + `rating/overall?chapterId=${chapterId}&seriesId=${seriesId}`); return this.httpClient.get<Rating>(this.baseUrl + `rating/overall-chapter?chapterId=${chapterId}`);
} }
return this.httpClient.get<Rating>(this.baseUrl + 'rating/overall?seriesId=' + seriesId);
return this.httpClient.get<Rating>(this.baseUrl + `rating/overall-series?seriesId=${seriesId}`);
} }
} }