Hooked up average rating for the issue, external ratings for individual issues (cbr only), and some polish.
Show Issues not Chapters for CBR matches.
This commit is contained in:
parent
6d4dfcda67
commit
da99c97813
21 changed files with 231 additions and 40 deletions
|
|
@ -423,6 +423,8 @@ public class ChapterController : BaseApiController
|
||||||
|
|
||||||
ret.Reviews = userReviews;
|
ret.Reviews = userReviews;
|
||||||
|
|
||||||
|
ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapterId);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@ public class ChapterDetailPlusDto
|
||||||
public float Rating { get; set; }
|
public float Rating { get; set; }
|
||||||
public bool HasBeenRated { get; set; }
|
public bool HasBeenRated { get; set; }
|
||||||
|
|
||||||
public List<UserReviewDto> Reviews { get; set; }
|
public IList<UserReviewDto> Reviews { get; set; } = [];
|
||||||
public List<RatingDto>? Ratings { get; set; }
|
public IList<RatingDto> Ratings { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,5 +57,8 @@ 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;
|
||||||
|
/// <summary>
|
||||||
|
/// Source of the Rating
|
||||||
|
/// </summary>
|
||||||
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
|
public RatingAuthority Authority { get; set; } = RatingAuthority.User;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
namespace API.Data.Migrations
|
namespace API.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(DataContext))]
|
[DbContext(typeof(DataContext))]
|
||||||
[Migration("20250428180534_ChapterRating")]
|
[Migration("20250429150140_ChapterRatingAndReviews")]
|
||||||
partial class ChapterRating
|
partial class ChapterRatingAndReviews
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
|
@ -790,6 +790,9 @@ namespace API.Data.Migrations
|
||||||
b.Property<string>("AlternateSeries")
|
b.Property<string>("AlternateSeries")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<float>("AverageExternalRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
b.Property<float>("AvgHoursToRead")
|
b.Property<float>("AvgHoursToRead")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
|
@ -1354,9 +1357,15 @@ namespace API.Data.Migrations
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Authority")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("AverageScore")
|
b.Property<int>("AverageScore")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("FavoriteCount")
|
b.Property<int>("FavoriteCount")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
|
@ -1371,6 +1380,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
b.ToTable("ExternalRating");
|
b.ToTable("ExternalRating");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2978,6 +2989,13 @@ namespace API.Data.Migrations
|
||||||
b.Navigation("Chapter");
|
b.Navigation("Chapter");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", null)
|
||||||
|
.WithMany("ExternalRatings")
|
||||||
|
.HasForeignKey("ChapterId");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
|
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Chapter", null)
|
b.HasOne("API.Entities.Chapter", null)
|
||||||
|
|
@ -3445,6 +3463,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("ExternalRatings");
|
||||||
|
|
||||||
b.Navigation("ExternalReviews");
|
b.Navigation("ExternalReviews");
|
||||||
|
|
||||||
b.Navigation("Files");
|
b.Navigation("Files");
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
namespace API.Data.Migrations
|
namespace API.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class ChapterRating : Migration
|
public partial class ChapterRatingAndReviews : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
|
@ -23,6 +23,26 @@ namespace API.Data.Migrations
|
||||||
type: "INTEGER",
|
type: "INTEGER",
|
||||||
nullable: true);
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Authority",
|
||||||
|
table: "ExternalRating",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "ChapterId",
|
||||||
|
table: "ExternalRating",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<float>(
|
||||||
|
name: "AverageExternalRating",
|
||||||
|
table: "Chapter",
|
||||||
|
type: "REAL",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0f);
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "AppUserChapterRating",
|
name: "AppUserChapterRating",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
|
|
@ -64,6 +84,11 @@ namespace API.Data.Migrations
|
||||||
table: "ExternalReview",
|
table: "ExternalReview",
|
||||||
column: "ChapterId");
|
column: "ChapterId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExternalRating_ChapterId",
|
||||||
|
table: "ExternalRating",
|
||||||
|
column: "ChapterId");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_AppUserChapterRating_AppUserId",
|
name: "IX_AppUserChapterRating_AppUserId",
|
||||||
table: "AppUserChapterRating",
|
table: "AppUserChapterRating",
|
||||||
|
|
@ -79,6 +104,13 @@ namespace API.Data.Migrations
|
||||||
table: "AppUserChapterRating",
|
table: "AppUserChapterRating",
|
||||||
column: "SeriesId");
|
column: "SeriesId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ExternalRating_Chapter_ChapterId",
|
||||||
|
table: "ExternalRating",
|
||||||
|
column: "ChapterId",
|
||||||
|
principalTable: "Chapter",
|
||||||
|
principalColumn: "Id");
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
migrationBuilder.AddForeignKey(
|
||||||
name: "FK_ExternalReview_Chapter_ChapterId",
|
name: "FK_ExternalReview_Chapter_ChapterId",
|
||||||
table: "ExternalReview",
|
table: "ExternalReview",
|
||||||
|
|
@ -90,6 +122,10 @@ namespace API.Data.Migrations
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ExternalRating_Chapter_ChapterId",
|
||||||
|
table: "ExternalRating");
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
name: "FK_ExternalReview_Chapter_ChapterId",
|
name: "FK_ExternalReview_Chapter_ChapterId",
|
||||||
table: "ExternalReview");
|
table: "ExternalReview");
|
||||||
|
|
@ -101,6 +137,10 @@ namespace API.Data.Migrations
|
||||||
name: "IX_ExternalReview_ChapterId",
|
name: "IX_ExternalReview_ChapterId",
|
||||||
table: "ExternalReview");
|
table: "ExternalReview");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ExternalRating_ChapterId",
|
||||||
|
table: "ExternalRating");
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
name: "Authority",
|
name: "Authority",
|
||||||
table: "ExternalReview");
|
table: "ExternalReview");
|
||||||
|
|
@ -108,6 +148,18 @@ namespace API.Data.Migrations
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
name: "ChapterId",
|
name: "ChapterId",
|
||||||
table: "ExternalReview");
|
table: "ExternalReview");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Authority",
|
||||||
|
table: "ExternalRating");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ChapterId",
|
||||||
|
table: "ExternalRating");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AverageExternalRating",
|
||||||
|
table: "Chapter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -787,6 +787,9 @@ namespace API.Data.Migrations
|
||||||
b.Property<string>("AlternateSeries")
|
b.Property<string>("AlternateSeries")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<float>("AverageExternalRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
b.Property<float>("AvgHoursToRead")
|
b.Property<float>("AvgHoursToRead")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
|
@ -1351,9 +1354,15 @@ namespace API.Data.Migrations
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Authority")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("AverageScore")
|
b.Property<int>("AverageScore")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("FavoriteCount")
|
b.Property<int>("FavoriteCount")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
|
@ -1368,6 +1377,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
b.ToTable("ExternalRating");
|
b.ToTable("ExternalRating");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2975,6 +2986,13 @@ namespace API.Data.Migrations
|
||||||
b.Navigation("Chapter");
|
b.Navigation("Chapter");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", null)
|
||||||
|
.WithMany("ExternalRatings")
|
||||||
|
.HasForeignKey("ChapterId");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
|
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Chapter", null)
|
b.HasOne("API.Entities.Chapter", null)
|
||||||
|
|
@ -3442,6 +3460,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("ExternalRatings");
|
||||||
|
|
||||||
b.Navigation("ExternalReviews");
|
b.Navigation("ExternalReviews");
|
||||||
|
|
||||||
b.Navigation("Files");
|
b.Navigation("Files");
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ public interface IChapterRepository
|
||||||
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
|
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
|
||||||
Task<int> GetAverageUserRating(int chapterId, int userId);
|
Task<int> GetAverageUserRating(int chapterId, int userId);
|
||||||
Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId);
|
Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId);
|
||||||
|
Task<IList<RatingDto>> GetExternalChapterRatings(int chapterId);
|
||||||
}
|
}
|
||||||
public class ChapterRepository : IChapterRepository
|
public class ChapterRepository : IChapterRepository
|
||||||
{
|
{
|
||||||
|
|
@ -340,4 +341,13 @@ public class ChapterRepository : IChapterRepository
|
||||||
.Select(r => _mapper.Map<UserReviewDto>(r))
|
.Select(r => _mapper.Map<UserReviewDto>(r))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<RatingDto>> GetExternalChapterRatings(int chapterId)
|
||||||
|
{
|
||||||
|
return await _context.Chapter
|
||||||
|
.Where(c => c.Id == chapterId)
|
||||||
|
.SelectMany(c => c.ExternalRatings)
|
||||||
|
.ProjectTo<RatingDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,11 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||||
public string WebLinks { get; set; } = string.Empty;
|
public string WebLinks { get; set; } = string.Empty;
|
||||||
public string ISBN { get; set; } = string.Empty;
|
public string ISBN { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// (Kavita+) Average rating from Kavita+ metadata
|
||||||
|
/// </summary>
|
||||||
|
public float AverageExternalRating { get; set; } = 0f;
|
||||||
|
|
||||||
#region Locks
|
#region Locks
|
||||||
|
|
||||||
public bool AgeRatingLocked { get; set; }
|
public bool AgeRatingLocked { get; set; }
|
||||||
|
|
@ -171,6 +176,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
|
|
||||||
public ICollection<ExternalReview> ExternalReviews { get; set; } = [];
|
public ICollection<ExternalReview> ExternalReviews { get; set; } = [];
|
||||||
|
public ICollection<ExternalRating> ExternalRatings { get; set; } = null!;
|
||||||
|
|
||||||
public void UpdateFrom(ParserInfo info)
|
public void UpdateFrom(ParserInfo info)
|
||||||
{
|
{
|
||||||
|
|
@ -196,8 +202,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string GetNumberTitle()
|
public string GetNumberTitle()
|
||||||
{
|
{
|
||||||
// BUG: TODO: On non-english locales, for floats, the range will be 20,5 but the NumberTitle will return 20.5
|
|
||||||
// Have I fixed this with TryParse CultureInvariant
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (MinNumber.Is(MaxNumber))
|
if (MinNumber.Is(MaxNumber))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace API.Entities.Enums;
|
namespace API.Entities.Enums;
|
||||||
|
|
||||||
public enum RatingAuthority
|
public enum RatingAuthority
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rating was from a User (internet or local)
|
||||||
|
/// </summary>
|
||||||
|
[Description("User")]
|
||||||
User = 0,
|
User = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// Rating was from Professional Critics
|
||||||
|
/// </summary>
|
||||||
|
[Description("Critic")]
|
||||||
Critic = 1,
|
Critic = 1,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
|
|
||||||
namespace API.Entities.Metadata;
|
namespace API.Entities.Metadata;
|
||||||
|
|
@ -10,8 +11,13 @@ public class ExternalRating
|
||||||
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; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This can be null when for a series-rating
|
||||||
|
/// </summary>
|
||||||
|
public int? ChapterId { get; set; }
|
||||||
|
|
||||||
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;
|
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1085,7 +1085,7 @@ 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 = UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification;
|
||||||
|
|
||||||
_unitOfWork.ChapterRepository.Update(chapter);
|
_unitOfWork.ChapterRepository.Update(chapter);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
@ -1094,20 +1094,20 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||||
return madeModification;
|
return madeModification;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata)
|
private 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)
|
if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("No external reviews found for chapter {ChapterID}", chapter.Id);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var madeModification = false;
|
||||||
|
|
||||||
|
#region Review
|
||||||
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews);
|
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews);
|
||||||
|
|
||||||
List<ExternalReview> externalReviews = [];
|
List<ExternalReview> externalReviews = [];
|
||||||
|
|
||||||
externalReviews.AddRange(metadata.CriticReviews
|
externalReviews.AddRange(metadata.CriticReviews
|
||||||
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
|
.Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body))
|
||||||
.Select(r =>
|
.Select(r =>
|
||||||
|
|
@ -1115,6 +1115,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||||
var review = _mapper.Map<ExternalReview>(r);
|
var review = _mapper.Map<ExternalReview>(r);
|
||||||
review.ChapterId = chapter.Id;
|
review.ChapterId = chapter.Id;
|
||||||
review.Authority = RatingAuthority.Critic;
|
review.Authority = RatingAuthority.Critic;
|
||||||
|
CleanCbrReview(ref review);
|
||||||
return review;
|
return review;
|
||||||
}));
|
}));
|
||||||
externalReviews.AddRange(metadata.UserReviews
|
externalReviews.AddRange(metadata.UserReviews
|
||||||
|
|
@ -1124,13 +1125,55 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||||
var review = _mapper.Map<ExternalReview>(r);
|
var review = _mapper.Map<ExternalReview>(r);
|
||||||
review.ChapterId = chapter.Id;
|
review.ChapterId = chapter.Id;
|
||||||
review.Authority = RatingAuthority.User;
|
review.Authority = RatingAuthority.User;
|
||||||
|
CleanCbrReview(ref review);
|
||||||
return review;
|
return review;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
chapter.ExternalReviews = externalReviews;
|
chapter.ExternalReviews = externalReviews;
|
||||||
|
madeModification = externalReviews.Count > 0;
|
||||||
_logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id);
|
_logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id);
|
||||||
return true;
|
#endregion
|
||||||
|
|
||||||
|
#region Rating
|
||||||
|
|
||||||
|
var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating);
|
||||||
|
var averageUserRating = metadata.UserReviews.Average(r => r.Rating);
|
||||||
|
|
||||||
|
_unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalRatings);
|
||||||
|
chapter.ExternalRatings =
|
||||||
|
[
|
||||||
|
new ExternalRating
|
||||||
|
{
|
||||||
|
AverageScore = (int) averageUserRating,
|
||||||
|
Provider = ScrobbleProvider.Cbr,
|
||||||
|
Authority = RatingAuthority.User,
|
||||||
|
ProviderUrl = metadata.IssueUrl,
|
||||||
|
},
|
||||||
|
new ExternalRating
|
||||||
|
{
|
||||||
|
AverageScore = (int) averageCriticRating,
|
||||||
|
Provider = ScrobbleProvider.Cbr,
|
||||||
|
Authority = RatingAuthority.Critic,
|
||||||
|
ProviderUrl = metadata.IssueUrl,
|
||||||
|
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
chapter.AverageExternalRating = averageUserRating;
|
||||||
|
|
||||||
|
madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return madeModification;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CleanCbrReview(ref ExternalReview review)
|
||||||
|
{
|
||||||
|
// CBR has Read Full Review which links to site, but we already have that
|
||||||
|
review.Body = review.Body.Replace("Read Full Review", string.Empty).TrimEnd();
|
||||||
|
review.RawBody = review.RawBody.Replace("Read Full Review", string.Empty).TrimEnd();
|
||||||
|
review.BodyJustText = review.BodyJustText.Replace("Read Full Review", string.Empty).TrimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import {ScrobbleProvider} from "../_services/scrobbling.service";
|
import {ScrobbleProvider} from "../_services/scrobbling.service";
|
||||||
|
|
||||||
|
export enum RatingAuthority {
|
||||||
|
User = 0,
|
||||||
|
Critic = 1,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Rating {
|
export interface Rating {
|
||||||
averageScore: number;
|
averageScore: number;
|
||||||
meanScore: number;
|
meanScore: number;
|
||||||
favoriteCount: number;
|
favoriteCount: number;
|
||||||
provider: ScrobbleProvider;
|
provider: ScrobbleProvider;
|
||||||
providerUrl: string | undefined;
|
providerUrl: string | undefined;
|
||||||
|
authority: RatingAuthority;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,11 @@
|
||||||
<div class="d-flex pt-3 justify-content-between">
|
<div class="d-flex pt-3 justify-content-between">
|
||||||
@if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) {
|
@if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) {
|
||||||
<span class="me-1">{{t('volume-count', {num: item.series.volumes})}}</span>
|
<span class="me-1">{{t('volume-count', {num: item.series.volumes})}}</span>
|
||||||
<span class="me-1">{{t('chapter-count', {num: item.series.chapters})}}</span>
|
@if (item.series.plusMediaFormat === PlusMediaFormat.Comic) {
|
||||||
|
<span class="me-1">{{t('issue-count', {num: item.series.chapters})}}</span>
|
||||||
|
} @else {
|
||||||
|
<span class="me-1">{{t('chapter-count', {num: item.series.chapters})}}</span>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<span class="me-1">{{t('releasing')}}</span>
|
<span class="me-1">{{t('releasing')}}</span>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe";
|
import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe";
|
||||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||||
|
import {PlusMediaFormat} from "../../_models/series-detail/external-series-detail";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-match-series-result-item',
|
selector: 'app-match-series-result-item',
|
||||||
|
|
@ -47,4 +48,5 @@ export class MatchSeriesResultItemComponent {
|
||||||
this.selected.emit(this.item);
|
this.selected.emit(this.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly PlusMediaFormat = PlusMediaFormat;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||||
|
import {RatingAuthority} from "../../_models/rating";
|
||||||
|
|
||||||
export enum RatingAuthority {
|
|
||||||
User = 0,
|
|
||||||
Critic = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserReview {
|
export interface UserReview {
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<div class="mt-2 mb-2">
|
<div class="mt-2 mb-2">
|
||||||
<app-external-rating [seriesId]="series.id"
|
<app-external-rating [seriesId]="series.id"
|
||||||
[ratings]="[]"
|
[ratings]="ratings"
|
||||||
[userRating]="rating"
|
[userRating]="rating"
|
||||||
[hasUserRated]="hasBeenRated"
|
[hasUserRated]="hasBeenRated"
|
||||||
[libraryType]="libraryType!"
|
[libraryType]="libraryType!"
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,28 @@
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component, DestroyRef,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
inject,
|
inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {AsyncPipe, DOCUMENT, NgStyle, NgClass, DatePipe, Location} from "@angular/common";
|
import {AsyncPipe, DatePipe, DOCUMENT, Location, NgClass, NgStyle} from "@angular/common";
|
||||||
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
|
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
|
||||||
import {LoadingComponent} from "../shared/loading/loading.component";
|
import {LoadingComponent} from "../shared/loading/loading.component";
|
||||||
import {
|
import {
|
||||||
NgbDropdown,
|
NgbDropdown,
|
||||||
NgbDropdownItem,
|
NgbDropdownItem,
|
||||||
NgbDropdownMenu,
|
NgbDropdownMenu,
|
||||||
NgbDropdownToggle, NgbModal,
|
NgbDropdownToggle,
|
||||||
NgbNav, NgbNavChangeEvent,
|
NgbModal,
|
||||||
NgbNavContent, NgbNavItem,
|
NgbNav,
|
||||||
NgbNavLink, NgbNavOutlet,
|
NgbNavChangeEvent,
|
||||||
|
NgbNavContent,
|
||||||
|
NgbNavItem,
|
||||||
|
NgbNavLink,
|
||||||
|
NgbNavOutlet,
|
||||||
NgbTooltip
|
NgbTooltip
|
||||||
} from "@ng-bootstrap/ng-bootstrap";
|
} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
|
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
|
||||||
|
|
@ -66,14 +71,10 @@ import {DefaultDatePipe} from "../_pipes/default-date.pipe";
|
||||||
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
||||||
import {DefaultModalOptions} from "../_models/default-modal-options";
|
import {DefaultModalOptions} from "../_models/default-modal-options";
|
||||||
import {UserReview} from "../_single-module/review-card/user-review";
|
import {UserReview} from "../_single-module/review-card/user-review";
|
||||||
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
|
|
||||||
import {ReviewCardComponent} from "../_single-module/review-card/review-card.component";
|
|
||||||
import {User} from "../_models/user";
|
import {User} from "../_models/user";
|
||||||
import {ReviewModalComponent} from "../_single-module/review-modal/review-modal.component";
|
|
||||||
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',
|
||||||
|
|
@ -149,6 +150,8 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
protected readonly TabID = TabID;
|
protected readonly TabID = TabID;
|
||||||
protected readonly FilterField = FilterField;
|
protected readonly FilterField = FilterField;
|
||||||
protected readonly Breakpoint = Breakpoint;
|
protected readonly Breakpoint = Breakpoint;
|
||||||
|
protected readonly LibraryType = LibraryType;
|
||||||
|
protected readonly encodeURIComponent = encodeURIComponent;
|
||||||
|
|
||||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||||
|
|
@ -165,6 +168,7 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
userReviews: Array<UserReview> = [];
|
userReviews: Array<UserReview> = [];
|
||||||
plusReviews: Array<UserReview> = [];
|
plusReviews: Array<UserReview> = [];
|
||||||
rating: number = 0;
|
rating: number = 0;
|
||||||
|
ratings: Array<Rating> = [];
|
||||||
hasBeenRated: boolean = false;
|
hasBeenRated: boolean = false;
|
||||||
|
|
||||||
weblinks: Array<string> = [];
|
weblinks: Array<string> = [];
|
||||||
|
|
@ -251,6 +255,7 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
this.plusReviews = results.chapterDetail.reviews.filter(r => r.isExternal);
|
this.plusReviews = results.chapterDetail.reviews.filter(r => r.isExternal);
|
||||||
this.rating = results.chapterDetail.rating;
|
this.rating = results.chapterDetail.rating;
|
||||||
this.hasBeenRated = results.chapterDetail.hasBeenRated;
|
this.hasBeenRated = results.chapterDetail.hasBeenRated;
|
||||||
|
this.ratings = results.chapterDetail.ratings;
|
||||||
|
|
||||||
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
|
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
|
||||||
|
|
||||||
|
|
@ -389,7 +394,4 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly LibraryType = LibraryType;
|
|
||||||
protected readonly encodeURIComponent = encodeURIComponent;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
@for (rating of ratings; track rating.provider + rating.averageScore) {
|
@for (rating of ratings; track rating.provider + rating.averageScore) {
|
||||||
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
|
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
|
||||||
[popoverTitle]="rating.provider | scrobbleProviderName" popoverClass="sm-popover">
|
[popoverTitle]="(rating.provider | scrobbleProviderName) + getAuthorityTitle(rating)" popoverClass="sm-popover">
|
||||||
<span class="badge rounded-pill me-1">
|
<span class="badge rounded-pill me-1">
|
||||||
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
|
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
|
||||||
{{rating.averageScore}}%
|
{{rating.averageScore}}%
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@if (rating.providerUrl) {
|
@if (rating.providerUrl) {
|
||||||
<a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a>
|
<a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ import {
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {SeriesService} from "../../../_services/series.service";
|
import {Rating, RatingAuthority} from "../../../_models/rating";
|
||||||
import {Rating} from "../../../_models/rating";
|
|
||||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||||
import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||||
|
|
@ -18,13 +17,12 @@ import {NgxStarsModule} from "ngx-stars";
|
||||||
import {ThemeService} from "../../../_services/theme.service";
|
import {ThemeService} from "../../../_services/theme.service";
|
||||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||||
import {ImageComponent} from "../../../shared/image/image.component";
|
import {ImageComponent} from "../../../shared/image/image.component";
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
|
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
|
||||||
import {ImageService} from "../../../_services/image.service";
|
import {ImageService} from "../../../_services/image.service";
|
||||||
import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common";
|
import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common";
|
||||||
import {RatingModalComponent} from "../rating-modal/rating-modal.component";
|
import {RatingModalComponent} from "../rating-modal/rating-modal.component";
|
||||||
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
|
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
|
||||||
import {ChapterService} from "../../../_services/chapter.service";
|
|
||||||
import {ReviewService} from "../../../_services/review.service";
|
import {ReviewService} from "../../../_services/review.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -86,4 +84,14 @@ export class ExternalRatingComponent implements OnInit {
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAuthorityTitle(rating: Rating) {
|
||||||
|
if (rating.authority === RatingAuthority.Critic) {
|
||||||
|
return ` (${translate('external-rating.critic')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly RatingAuthority = RatingAuthority;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@
|
||||||
[mangaFormat]="series.format">
|
[mangaFormat]="series.format">
|
||||||
</app-metadata-detail-row>
|
</app-metadata-detail-row>
|
||||||
|
|
||||||
<!-- Rating goes here (after I implement support for rating individual issues -->
|
|
||||||
<div class="mt-2 mb-2">
|
<div class="mt-2 mb-2">
|
||||||
<app-external-rating [seriesId]="series.id"
|
<app-external-rating [seriesId]="series.id"
|
||||||
[ratings]="ratings"
|
[ratings]="ratings"
|
||||||
|
|
|
||||||
|
|
@ -1011,6 +1011,7 @@
|
||||||
"match-series-result-item": {
|
"match-series-result-item": {
|
||||||
"volume-count": "{{server-stats.volume-count}}",
|
"volume-count": "{{server-stats.volume-count}}",
|
||||||
"chapter-count": "{{common.chapter-count}}",
|
"chapter-count": "{{common.chapter-count}}",
|
||||||
|
"issue-count": "{{common.issue-count}}",
|
||||||
"releasing": "Releasing",
|
"releasing": "Releasing",
|
||||||
"details": "View page",
|
"details": "View page",
|
||||||
"updating-metadata-status": "Updating Metadata"
|
"updating-metadata-status": "Updating Metadata"
|
||||||
|
|
@ -1048,7 +1049,8 @@
|
||||||
"entry-label": "See Details",
|
"entry-label": "See Details",
|
||||||
"kavita-tooltip": "Your Rating + Overall",
|
"kavita-tooltip": "Your Rating + Overall",
|
||||||
"kavita-rating-title": "Your Rating",
|
"kavita-rating-title": "Your Rating",
|
||||||
"close": "{{common.close}}"
|
"close": "{{common.close}}",
|
||||||
|
"critic": "Critic"
|
||||||
},
|
},
|
||||||
|
|
||||||
"badge-expander": {
|
"badge-expander": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue