Ingest ExternalReviews from K+
Adds a new entity ExternalChapterMetadata, which would allow us to extend chapters to Recommendations, Ratings, etc in the future
This commit is contained in:
parent
749fb24185
commit
052b3f9fe4
29 changed files with 647 additions and 137 deletions
|
|
@ -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<ChapterController> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger<ChapterController> logger)
|
||||
public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger<ChapterController> logger,
|
||||
IMapper mapper)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
_eventHub = eventHub;
|
||||
_logger = logger;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -392,15 +397,34 @@ public class ChapterController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all reviews for this chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("review")]
|
||||
public async Task<IList<UserReviewDto>> ChapterReviews([FromQuery] int chapterId)
|
||||
|
||||
[HttpGet("chapter-detail-plus")]
|
||||
public async Task<ChapterDetailPlusDto> 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<UserReviewDto>(ex)).ToList();
|
||||
userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(dtos));
|
||||
}
|
||||
|
||||
ret.Reviews = userReviews;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,21 +30,6 @@ public class ReviewController : BaseApiController
|
|||
_scrobblingService = scrobblingService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all reviews for the series, or chapter
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IList<UserReviewDto>> 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());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
|
|
|
|||
14
API/DTOs/ChapterDetailPlusDto.cs
Normal file
14
API/DTOs/ChapterDetailPlusDto.cs
Normal file
|
|
@ -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<UserReviewDto> Reviews { get; set; }
|
||||
public List<RatingDto>? Ratings { get; set; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
|
|
@ -58,4 +58,5 @@ public class UserReviewDto
|
|||
/// If this review is External, which Provider did it come from
|
||||
/// </summary>
|
||||
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita;
|
||||
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<EmailHistory> EmailHistory { get; set; } = null!;
|
||||
public DbSet<MetadataSettings> MetadataSettings { get; set; } = null!;
|
||||
public DbSet<MetadataFieldMapping> MetadataFieldMapping { get; set; } = null!;
|
||||
public DbSet<ExternalChapterReview> ExternalChapterReview { get; set; } = null!;
|
||||
public DbSet<ExternalChapterMetadata> ExternalChapterMetadata { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,48 +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.AddColumn<int>(
|
||||
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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -1318,6 +1318,70 @@ namespace API.Data.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -2456,6 +2520,21 @@ namespace API.Data.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<int>("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");
|
||||
135
API/Data/Migrations/20250428141809_ChapterRating.cs
Normal file
135
API/Data/Migrations/20250428141809_ChapterRating.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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: "ChapterId",
|
||||
table: "AppUserRating",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
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_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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1315,6 +1315,70 @@ namespace API.Data.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -2453,6 +2517,21 @@ namespace API.Data.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<int>("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");
|
||||
|
|
|
|||
45
API/Data/Repositories/ExternalChapterMetadataRepository.cs
Normal file
45
API/Data/Repositories/ExternalChapterMetadataRepository.cs
Normal file
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ public interface IUnitOfWork
|
|||
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||
IEmailHistoryRepository EmailHistoryRepository { get; }
|
||||
IExternalChapterMetadataRepository ExternalChapterMetadataRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,6 +105,7 @@ public class UnitOfWork : IUnitOfWork
|
|||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||
public IEmailHistoryRepository EmailHistoryRepository { get; }
|
||||
public IExternalChapterMetadataRepository ExternalChapterMetadataRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
|
|
|||
|
|
@ -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<MangaFile>();
|
||||
|
|
|
|||
20
API/Entities/Metadata/ExternalChapterMetadata.cs
Normal file
20
API/Entities/Metadata/ExternalChapterMetadata.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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!;
|
||||
|
||||
}
|
||||
45
API/Entities/Metadata/ExternalChapterReview.cs
Normal file
45
API/Entities/Metadata/ExternalChapterReview.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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!;
|
||||
|
||||
}
|
||||
|
|
@ -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<ExternalChapterMetadata> Includes(this IQueryable<ExternalChapterMetadata> query, ExternalChapterMetadataIncludes includeFlags)
|
||||
{
|
||||
if (includeFlags.HasFlag(ExternalChapterMetadataIncludes.ExternalReviews))
|
||||
{
|
||||
query = query.Include(e => e.ExternalReviews);
|
||||
}
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,11 +334,19 @@ public class AutoMapperProfiles : Profile
|
|||
.ForMember(dest => dest.IsExternal,
|
||||
opt =>
|
||||
opt.MapFrom(src => true));
|
||||
CreateMap<ExternalChapterReview, UserReviewDto>()
|
||||
.ForMember(dest => dest.IsExternal,
|
||||
opt =>
|
||||
opt.MapFrom(src => true));
|
||||
|
||||
CreateMap<UserReviewDto, ExternalReview>()
|
||||
.ForMember(dest => dest.BodyJustText,
|
||||
opt =>
|
||||
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<Series, ManageMatchSeriesDto>()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ public class RatingBuilder : IEntityBuilder<AppUserRating>
|
|||
return this;
|
||||
}
|
||||
|
||||
public RatingBuilder WithChapterId(int? chapterId)
|
||||
{
|
||||
_rating.ChapterId = chapterId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RatingBuilder WithRating(int rating)
|
||||
{
|
||||
_rating.Rating = Math.Clamp(rating, 0, 5);
|
||||
|
|
|
|||
|
|
@ -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<bool> UpdateChapterReviews(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata)
|
||||
private async Task<bool> 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<ExternalChapterReview> externalReviews = [];
|
||||
|
||||
externalReviews.AddRange(metadata.CriticReviews
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
|
||||
.Select(r =>
|
||||
{
|
||||
var review = _mapper.Map<ExternalChapterReview>(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<ExternalChapterReview>(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;
|
||||
}
|
||||
|
||||
/// <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,
|
||||
ExternalSeriesMetadata externalSeriesMetadata)
|
||||
{
|
||||
|
|
|
|||
9
UI/Web/src/app/_models/chapter-detail.ts
Normal file
9
UI/Web/src/app/_models/chapter-detail.ts
Normal file
|
|
@ -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[];
|
||||
};
|
||||
|
|
@ -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<Array<UserReview>>(this.baseUrl + 'chapter/review?chapterId='+chapterId);
|
||||
chapterDetailPlus(seriesId: number, chapterId: number) {
|
||||
return this.httpClient.get<ChapterDetail>(this.baseUrl + `chapter/chapter-detail-plus?chapterId=${chapterId}&seriesId=${seriesId}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,6 @@ export class ReviewService {
|
|||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getReviews(seriesId: number, chapterId?: number) {
|
||||
if (chapterId) {
|
||||
return this.httpClient.get<UserReview[]>(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`);
|
||||
}
|
||||
return this.httpClient.get<UserReview[]>(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<UserReview>(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`, {
|
||||
rating, body
|
||||
return this.httpClient.post<UserReview>(this.baseUrl + `review`, {
|
||||
seriesId, chapterId, body
|
||||
});
|
||||
}
|
||||
|
||||
return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
|
||||
seriesId, rating, body
|
||||
seriesId, body
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,10 @@
|
|||
|
||||
|
||||
<div class="mt-2 mb-2">
|
||||
@let rating = userRating();
|
||||
<app-external-rating [seriesId]="series.id"
|
||||
[ratings]="[]"
|
||||
[userRating]="rating?.rating || 0"
|
||||
[hasUserRated]="rating !== undefined && rating.hasBeenRated"
|
||||
[userRating]="rating"
|
||||
[hasUserRated]="hasBeenRated"
|
||||
[libraryType]="libraryType!"
|
||||
[chapterId]="chapterId"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<UserReview> = [];
|
||||
plusReviews: Array<UserReview> = [];
|
||||
rating: number = 0;
|
||||
hasBeenRated: boolean = false;
|
||||
|
||||
weblinks: Array<string> = [];
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,10 @@
|
|||
|
||||
@if (libraryType !== null && series && volume.chapters.length === 1) {
|
||||
<div class="mt-2 mb-2">
|
||||
@let rating = userRating();
|
||||
<app-external-rating [seriesId]="series.id"
|
||||
[ratings]="[]"
|
||||
[userRating]="rating?.rating || 0"
|
||||
[hasUserRated]="rating !== undefined && rating.hasBeenRated"
|
||||
[userRating]="rating"
|
||||
[hasUserRated]="hasBeenRated"
|
||||
[libraryType]="libraryType"
|
||||
[chapterId]="volume.chapters[0].id"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<UserReview> = [];
|
||||
plusReviews: Array<UserReview> = [];
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue