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;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Nager.ArticleNumber;
|
using Nager.ArticleNumber;
|
||||||
|
|
||||||
|
|
@ -28,13 +30,16 @@ public class ChapterController : BaseApiController
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly ILogger<ChapterController> _logger;
|
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;
|
_unitOfWork = unitOfWork;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -392,15 +397,34 @@ public class ChapterController : BaseApiController
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all reviews for this chapter
|
[HttpGet("chapter-detail-plus")]
|
||||||
/// </summary>
|
public async Task<ChapterDetailPlusDto> ChapterDetailPlus([FromQuery] int seriesId, [FromQuery] int chapterId)
|
||||||
/// <param name="chapterId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet("review")]
|
|
||||||
public async Task<IList<UserReviewDto>> ChapterReviews([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;
|
_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>
|
/// <summary>
|
||||||
/// Updates the review for a given series, or chapter
|
/// Updates the review for a given series, or chapter
|
||||||
|
|
@ -62,8 +47,8 @@ public class ReviewController : BaseApiController
|
||||||
var rating = ratingBuilder
|
var rating = ratingBuilder
|
||||||
.WithBody(dto.Body)
|
.WithBody(dto.Body)
|
||||||
.WithSeriesId(dto.SeriesId)
|
.WithSeriesId(dto.SeriesId)
|
||||||
|
.WithChapterId(dto.ChapterId)
|
||||||
.WithTagline(string.Empty)
|
.WithTagline(string.Empty)
|
||||||
.WithRating(dto.Rating)
|
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
if (rating.Id == 0)
|
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;
|
namespace API.DTOs;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -8,5 +9,6 @@ public class RatingDto
|
||||||
public int AverageScore { get; set; }
|
public int AverageScore { get; set; }
|
||||||
public int FavoriteCount { get; set; }
|
public int FavoriteCount { get; set; }
|
||||||
public ScrobbleProvider Provider { get; set; }
|
public ScrobbleProvider Provider { get; set; }
|
||||||
|
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
|
||||||
public string? ProviderUrl { get; set; }
|
public string? ProviderUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace API.DTOs.SeriesDetail;
|
namespace API.DTOs.SeriesDetail;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
|
@ -7,6 +6,5 @@ public class UpdateUserReviewDto
|
||||||
{
|
{
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
public int? ChapterId { get; set; }
|
public int? ChapterId { get; set; }
|
||||||
public int Rating { get; set; }
|
|
||||||
public string Body { get; set; }
|
public string Body { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
|
|
||||||
namespace API.DTOs.SeriesDetail;
|
namespace API.DTOs.SeriesDetail;
|
||||||
|
|
@ -38,7 +39,6 @@ public class UserReviewDto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public int TotalVotes { get; set; }
|
public int TotalVotes { get; set; }
|
||||||
public float Rating { get; set; }
|
|
||||||
public bool HasBeenRated { get; set; }
|
public bool HasBeenRated { get; set; }
|
||||||
public string? RawBody { get; set; }
|
public string? RawBody { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -58,4 +58,5 @@ public class UserReviewDto
|
||||||
/// If this review is External, which Provider did it come from
|
/// If this review is External, which Provider did it come from
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita;
|
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<EmailHistory> EmailHistory { get; set; } = null!;
|
||||||
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<ExternalChapterReview> ExternalChapterReview { get; set; } = null!;
|
||||||
|
public DbSet<ExternalChapterMetadata> ExternalChapterMetadata { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
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
|
namespace API.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(DataContext))]
|
[DbContext(typeof(DataContext))]
|
||||||
[Migration("20250426173850_ChapterRating")]
|
[Migration("20250428141809_ChapterRating")]
|
||||||
partial class ChapterRating
|
partial class ChapterRating
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -1318,6 +1318,70 @@ 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")
|
||||||
|
|
@ -2456,6 +2520,21 @@ 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")
|
||||||
|
|
@ -2919,6 +2998,15 @@ namespace API.Data.Migrations
|
||||||
b.Navigation("Chapter");
|
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 =>
|
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Series", "Series")
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
|
@ -3226,6 +3314,21 @@ 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)
|
||||||
|
|
@ -3377,6 +3480,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("ExternalChapterMetadata");
|
||||||
|
|
||||||
b.Navigation("Files");
|
b.Navigation("Files");
|
||||||
|
|
||||||
b.Navigation("People");
|
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");
|
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")
|
||||||
|
|
@ -2453,6 +2517,21 @@ 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")
|
||||||
|
|
@ -2916,6 +2995,15 @@ namespace API.Data.Migrations
|
||||||
b.Navigation("Chapter");
|
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 =>
|
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Series", "Series")
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
|
@ -3223,6 +3311,21 @@ 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)
|
||||||
|
|
@ -3374,6 +3477,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("ExternalChapterMetadata");
|
||||||
|
|
||||||
b.Navigation("Files");
|
b.Navigation("Files");
|
||||||
|
|
||||||
b.Navigation("People");
|
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; }
|
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();
|
||||||
|
|
@ -74,6 +75,7 @@ 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>
|
||||||
|
|
@ -103,6 +105,7 @@ 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.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
using API.Entities.Metadata;
|
||||||
using API.Entities.Person;
|
using API.Entities.Person;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
@ -169,6 +170,8 @@ 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 void UpdateFrom(ParserInfo info)
|
public void UpdateFrom(ParserInfo info)
|
||||||
{
|
{
|
||||||
Files ??= new List<MangaFile>();
|
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 System.Linq;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Metadata;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Extensions.QueryExtensions;
|
namespace API.Extensions.QueryExtensions;
|
||||||
|
|
@ -303,4 +304,14 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -334,11 +334,19 @@ 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>()
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ public class RatingBuilder : IEntityBuilder<AppUserRating>
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RatingBuilder WithChapterId(int? chapterId)
|
||||||
|
{
|
||||||
|
_rating.ChapterId = chapterId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public RatingBuilder WithRating(int rating)
|
public RatingBuilder WithRating(int rating)
|
||||||
{
|
{
|
||||||
_rating.Rating = Math.Clamp(rating, 0, 5);
|
_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 UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification;
|
||||||
|
|
||||||
madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification;
|
madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification;
|
||||||
|
madeModification = await UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification;
|
||||||
madeModification = await UpdateChapterReviews(chapter, settings, potentialMatch) || madeModification;
|
|
||||||
|
|
||||||
_unitOfWork.ChapterRepository.Update(chapter);
|
_unitOfWork.ChapterRepository.Update(chapter);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return madeModification;
|
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 (!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
|
var exteralChapterMetadata = await GetOrCreateExternalChapterMetadataForChapter(chapter.Id, chapter);
|
||||||
chapter.Ratings.Clear();
|
_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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1562,6 +1588,28 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
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 {TextResonse} from "../_types/text-response";
|
||||||
import {UserReview} from "../_single-module/review-card/user-review";
|
import {UserReview} from "../_single-module/review-card/user-review";
|
||||||
import {Rating} from "../_models/rating";
|
import {Rating} from "../_models/rating";
|
||||||
|
import {ChapterDetail} from "../_models/chapter-detail";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -31,8 +32,8 @@ export class ChapterService {
|
||||||
return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse);
|
return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse);
|
||||||
}
|
}
|
||||||
|
|
||||||
chapterReviews(chapterId: number) {
|
chapterDetailPlus(seriesId: number, chapterId: number) {
|
||||||
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'chapter/review?chapterId='+chapterId);
|
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) { }
|
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) {
|
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?chapterId=${chapterId}&seriesId=${seriesId}`);
|
||||||
|
|
@ -28,15 +21,15 @@ export class ReviewService {
|
||||||
return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId);
|
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) {
|
if (chapterId) {
|
||||||
return this.httpClient.post<UserReview>(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`, {
|
return this.httpClient.post<UserReview>(this.baseUrl + `review`, {
|
||||||
rating, body
|
seriesId, chapterId, body
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
|
return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
|
||||||
seriesId, rating, body
|
seriesId, body
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||||
|
|
||||||
|
export enum RatingAuthority {
|
||||||
|
User = 0,
|
||||||
|
Critic = 1
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserReview {
|
export interface UserReview {
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
libraryId: number;
|
libraryId: number;
|
||||||
volumeId?: number;
|
|
||||||
chapterId?: number;
|
chapterId?: number;
|
||||||
rating: number;
|
|
||||||
hasBeenRated: boolean;
|
|
||||||
score: number;
|
score: number;
|
||||||
username: string;
|
username: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
|
@ -15,4 +17,5 @@ export interface UserReview {
|
||||||
bodyJustText?: string;
|
bodyJustText?: string;
|
||||||
siteUrl?: string;
|
siteUrl?: string;
|
||||||
provider: ScrobbleProvider;
|
provider: ScrobbleProvider;
|
||||||
|
authority: RatingAuthority;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,31 +35,21 @@ export class ReviewModalComponent implements OnInit {
|
||||||
|
|
||||||
protected readonly modal = inject(NgbActiveModal);
|
protected readonly modal = inject(NgbActiveModal);
|
||||||
private readonly reviewService = inject(ReviewService);
|
private readonly reviewService = inject(ReviewService);
|
||||||
private readonly seriesService = inject(SeriesService);
|
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly confirmService = inject(ConfirmService);
|
private readonly confirmService = inject(ConfirmService);
|
||||||
private readonly toastr = inject(ToastrService);
|
private readonly toastr = inject(ToastrService);
|
||||||
private readonly themeService = inject(ThemeService);
|
|
||||||
protected readonly minLength = 5;
|
protected readonly minLength = 5;
|
||||||
|
|
||||||
@Input({required: true}) review!: UserReview;
|
@Input({required: true}) review!: UserReview;
|
||||||
reviewGroup!: FormGroup;
|
reviewGroup!: FormGroup;
|
||||||
rating: number = 0;
|
|
||||||
|
|
||||||
starColor = this.themeService.getCssVariable('--rating-star-color');
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.reviewGroup = new FormGroup({
|
this.reviewGroup = new FormGroup({
|
||||||
reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]),
|
reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]),
|
||||||
});
|
});
|
||||||
this.rating = this.review.rating;
|
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRating($event: number) {
|
|
||||||
this.rating = $event;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.modal.close({success: false, review: this.review, action: ReviewModalCloseAction.Close});
|
this.modal.close({success: false, review: this.review, action: ReviewModalCloseAction.Close});
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +69,7 @@ export class ReviewModalComponent implements OnInit {
|
||||||
return;
|
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});
|
this.modal.close({success: true, review: review, action: ReviewModalCloseAction.Edit});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,10 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-2 mb-2">
|
<div class="mt-2 mb-2">
|
||||||
@let rating = userRating();
|
|
||||||
<app-external-rating [seriesId]="series.id"
|
<app-external-rating [seriesId]="series.id"
|
||||||
[ratings]="[]"
|
[ratings]="[]"
|
||||||
[userRating]="rating?.rating || 0"
|
[userRating]="rating"
|
||||||
[hasUserRated]="rating !== undefined && rating.hasBeenRated"
|
[hasUserRated]="hasBeenRated"
|
||||||
[libraryType]="libraryType!"
|
[libraryType]="libraryType!"
|
||||||
[chapterId]="chapterId"
|
[chapterId]="chapterId"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,11 @@ import {ReviewModalComponent} from "../_single-module/review-modal/review-modal.
|
||||||
import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
|
import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
|
||||||
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
|
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
|
||||||
import {Rating} from "../_models/rating";
|
import {Rating} from "../_models/rating";
|
||||||
|
import {ReviewService} from "../_services/review.service";
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
Related = 'related-tab',
|
Related = 'related-tab',
|
||||||
Reviews = 'review-tab', // Only applicable for books
|
Reviews = 'review-tab',
|
||||||
Details = 'details-tab'
|
Details = 'details-tab'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,8 +112,6 @@ enum TabID {
|
||||||
DatePipe,
|
DatePipe,
|
||||||
DefaultDatePipe,
|
DefaultDatePipe,
|
||||||
CoverImageComponent,
|
CoverImageComponent,
|
||||||
CarouselReelComponent,
|
|
||||||
ReviewCardComponent,
|
|
||||||
ReviewsComponent,
|
ReviewsComponent,
|
||||||
ExternalRatingComponent
|
ExternalRatingComponent
|
||||||
],
|
],
|
||||||
|
|
@ -165,6 +164,9 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
hasReadingProgress = false;
|
hasReadingProgress = false;
|
||||||
userReviews: Array<UserReview> = [];
|
userReviews: Array<UserReview> = [];
|
||||||
plusReviews: Array<UserReview> = [];
|
plusReviews: Array<UserReview> = [];
|
||||||
|
rating: number = 0;
|
||||||
|
hasBeenRated: boolean = false;
|
||||||
|
|
||||||
weblinks: Array<string> = [];
|
weblinks: Array<string> = [];
|
||||||
activeTabId = TabID.Details;
|
activeTabId = TabID.Details;
|
||||||
/**
|
/**
|
||||||
|
|
@ -233,7 +235,7 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
series: this.seriesService.getSeries(this.seriesId),
|
series: this.seriesService.getSeries(this.seriesId),
|
||||||
chapter: this.chapterService.getChapterMetadata(this.chapterId),
|
chapter: this.chapterService.getChapterMetadata(this.chapterId),
|
||||||
libraryType: this.libraryService.getLibraryType(this.libraryId),
|
libraryType: this.libraryService.getLibraryType(this.libraryId),
|
||||||
reviews: this.chapterService.chapterReviews(this.chapterId),
|
chapterDetail: this.chapterService.chapterDetailPlus(this.seriesId, this.chapterId),
|
||||||
}).subscribe(results => {
|
}).subscribe(results => {
|
||||||
|
|
||||||
if (results.chapter === null) {
|
if (results.chapter === null) {
|
||||||
|
|
@ -245,8 +247,10 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
this.chapter = results.chapter;
|
this.chapter = results.chapter;
|
||||||
this.weblinks = this.chapter.webLinks.split(',');
|
this.weblinks = this.chapter.webLinks.split(',');
|
||||||
this.libraryType = results.libraryType;
|
this.libraryType = results.libraryType;
|
||||||
this.userReviews = results.reviews.filter(r => !r.isExternal);
|
this.userReviews = results.chapterDetail.reviews.filter(r => !r.isExternal);
|
||||||
this.plusReviews = results.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);
|
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 LibraryType = LibraryType;
|
||||||
protected readonly encodeURIComponent = encodeURIComponent;
|
protected readonly encodeURIComponent = encodeURIComponent;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,10 @@
|
||||||
|
|
||||||
@if (libraryType !== null && series && volume.chapters.length === 1) {
|
@if (libraryType !== null && series && volume.chapters.length === 1) {
|
||||||
<div class="mt-2 mb-2">
|
<div class="mt-2 mb-2">
|
||||||
@let rating = userRating();
|
|
||||||
<app-external-rating [seriesId]="series.id"
|
<app-external-rating [seriesId]="series.id"
|
||||||
[ratings]="[]"
|
[ratings]="[]"
|
||||||
[userRating]="rating?.rating || 0"
|
[userRating]="rating"
|
||||||
[hasUserRated]="rating !== undefined && rating.hasBeenRated"
|
[hasUserRated]="hasBeenRated"
|
||||||
[libraryType]="libraryType"
|
[libraryType]="libraryType"
|
||||||
[chapterId]="volume.chapters[0].id"
|
[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 {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
|
||||||
import {User} from "../_models/user";
|
import {User} from "../_models/user";
|
||||||
import {ReviewService} from "../_services/review.service";
|
import {ReviewService} from "../_services/review.service";
|
||||||
|
import {ChapterService} from "../_services/chapter.service";
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
|
|
||||||
|
|
@ -183,7 +184,7 @@ export class VolumeDetailComponent implements OnInit {
|
||||||
private readonly readingListService = inject(ReadingListService);
|
private readonly readingListService = inject(ReadingListService);
|
||||||
private readonly messageHub = inject(MessageHubService);
|
private readonly messageHub = inject(MessageHubService);
|
||||||
private readonly location = inject(Location);
|
private readonly location = inject(Location);
|
||||||
private readonly reviewService = inject(ReviewService);
|
private readonly chapterService = inject(ChapterService);
|
||||||
|
|
||||||
|
|
||||||
protected readonly AgeRating = AgeRating;
|
protected readonly AgeRating = AgeRating;
|
||||||
|
|
@ -204,8 +205,13 @@ export class VolumeDetailComponent implements OnInit {
|
||||||
libraryType: LibraryType | null = null;
|
libraryType: LibraryType | null = null;
|
||||||
activeTabId = TabID.Chapters;
|
activeTabId = TabID.Chapters;
|
||||||
readingLists: ReadingList[] = [];
|
readingLists: ReadingList[] = [];
|
||||||
|
|
||||||
|
// Only populated if the volume has exactly one chapter
|
||||||
userReviews: Array<UserReview> = [];
|
userReviews: Array<UserReview> = [];
|
||||||
plusReviews: Array<UserReview> = [];
|
plusReviews: Array<UserReview> = [];
|
||||||
|
rating: number = 0;
|
||||||
|
hasBeenRated: boolean = false;
|
||||||
|
|
||||||
mobileSeriesImgBackground: string | undefined;
|
mobileSeriesImgBackground: string | undefined;
|
||||||
downloadInProgress: boolean = false;
|
downloadInProgress: boolean = false;
|
||||||
|
|
||||||
|
|
@ -405,9 +411,11 @@ export class VolumeDetailComponent implements OnInit {
|
||||||
this.libraryType = results.libraryType;
|
this.libraryType = results.libraryType;
|
||||||
|
|
||||||
if (this.volume.chapters.length === 1) {
|
if (this.volume.chapters.length === 1) {
|
||||||
this.reviewService.getReviews(this.seriesId, this.volume.chapters[0].id).subscribe(reviews => {
|
this.chapterService.chapterDetailPlus(this.seriesId, this.volume.chapters[0].id).subscribe(detail => {
|
||||||
this.userReviews = reviews.filter(r => !r.isExternal);
|
this.userReviews = detail.reviews.filter(r => !r.isExternal);
|
||||||
this.plusReviews = 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;
|
protected readonly encodeURIComponent = encodeURIComponent;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue