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:
Amelia 2025-04-28 16:19:03 +02:00
parent 749fb24185
commit 052b3f9fe4
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
29 changed files with 647 additions and 137 deletions

View file

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

View file

@ -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");
}
}
}

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20250426173850_ChapterRating")]
[Migration("20250428141809_ChapterRating")]
partial class ChapterRating
{
/// <inheritdoc />
@ -1318,6 +1318,70 @@ namespace API.Data.Migrations
b.ToTable("MediaError");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId")
.IsUnique();
b.ToTable("ExternalChapterMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterReview", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<string>("Body")
.HasColumnType("TEXT");
b.Property<string>("BodyJustText")
.HasColumnType("TEXT");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Provider")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("RawBody")
.HasColumnType("TEXT");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.Property<string>("SiteUrl")
.HasColumnType("TEXT");
b.Property<string>("Tagline")
.HasColumnType("TEXT");
b.Property<int>("TotalVotes")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ExternalChapterReview");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
{
b.Property<int>("Id")
@ -2456,6 +2520,21 @@ namespace API.Data.Migrations
b.ToTable("CollectionTagSeriesMetadata");
});
modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b =>
{
b.Property<int>("ExternalChapterMetadatasId")
.HasColumnType("INTEGER");
b.Property<int>("ExternalReviewsId")
.HasColumnType("INTEGER");
b.HasKey("ExternalChapterMetadatasId", "ExternalReviewsId");
b.HasIndex("ExternalReviewsId");
b.ToTable("ExternalChapterMetadataExternalChapterReview");
});
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{
b.Property<int>("ExternalRatingsId")
@ -2919,6 +2998,15 @@ namespace API.Data.Migrations
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b =>
{
b.HasOne("API.Entities.Chapter", null)
.WithOne("ExternalChapterMetadata")
.HasForeignKey("API.Entities.Metadata.ExternalChapterMetadata", "ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
{
b.HasOne("API.Entities.Series", "Series")
@ -3226,6 +3314,21 @@ namespace API.Data.Migrations
.IsRequired();
});
modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b =>
{
b.HasOne("API.Entities.Metadata.ExternalChapterMetadata", null)
.WithMany()
.HasForeignKey("ExternalChapterMetadatasId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Metadata.ExternalChapterReview", null)
.WithMany()
.HasForeignKey("ExternalReviewsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{
b.HasOne("API.Entities.Metadata.ExternalRating", null)
@ -3377,6 +3480,8 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("ExternalChapterMetadata");
b.Navigation("Files");
b.Navigation("People");

View 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");
}
}
}

View file

@ -1315,6 +1315,70 @@ namespace API.Data.Migrations
b.ToTable("MediaError");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId")
.IsUnique();
b.ToTable("ExternalChapterMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterReview", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Authority")
.HasColumnType("INTEGER");
b.Property<string>("Body")
.HasColumnType("TEXT");
b.Property<string>("BodyJustText")
.HasColumnType("TEXT");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("Provider")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("RawBody")
.HasColumnType("TEXT");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.Property<string>("SiteUrl")
.HasColumnType("TEXT");
b.Property<string>("Tagline")
.HasColumnType("TEXT");
b.Property<int>("TotalVotes")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ExternalChapterReview");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
{
b.Property<int>("Id")
@ -2453,6 +2517,21 @@ namespace API.Data.Migrations
b.ToTable("CollectionTagSeriesMetadata");
});
modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b =>
{
b.Property<int>("ExternalChapterMetadatasId")
.HasColumnType("INTEGER");
b.Property<int>("ExternalReviewsId")
.HasColumnType("INTEGER");
b.HasKey("ExternalChapterMetadatasId", "ExternalReviewsId");
b.HasIndex("ExternalReviewsId");
b.ToTable("ExternalChapterMetadataExternalChapterReview");
});
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{
b.Property<int>("ExternalRatingsId")
@ -2916,6 +2995,15 @@ namespace API.Data.Migrations
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Entities.Metadata.ExternalChapterMetadata", b =>
{
b.HasOne("API.Entities.Chapter", null)
.WithOne("ExternalChapterMetadata")
.HasForeignKey("API.Entities.Metadata.ExternalChapterMetadata", "ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
{
b.HasOne("API.Entities.Series", "Series")
@ -3223,6 +3311,21 @@ namespace API.Data.Migrations
.IsRequired();
});
modelBuilder.Entity("ExternalChapterMetadataExternalChapterReview", b =>
{
b.HasOne("API.Entities.Metadata.ExternalChapterMetadata", null)
.WithMany()
.HasForeignKey("ExternalChapterMetadatasId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Metadata.ExternalChapterReview", null)
.WithMany()
.HasForeignKey("ExternalReviewsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
{
b.HasOne("API.Entities.Metadata.ExternalRating", null)
@ -3374,6 +3477,8 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("ExternalChapterMetadata");
b.Navigation("Files");
b.Navigation("People");

View 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);
}
}

View file

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