New Scanner + People Pages (#3286)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
1ed0eae22d
commit
ba20ad4ecc
142 changed files with 17529 additions and 3038 deletions
|
|
@ -66,6 +66,8 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
|
||||
public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!;
|
||||
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
|
||||
public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!;
|
||||
public DbSet<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
|
@ -155,6 +157,36 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserCollection>()
|
||||
.Property(b => b.AgeRating)
|
||||
.HasDefaultValue(AgeRating.Unknown);
|
||||
|
||||
// Configure the many-to-many relationship for Movie and Person
|
||||
builder.Entity<ChapterPeople>()
|
||||
.HasKey(cp => new { cp.ChapterId, cp.PersonId, cp.Role });
|
||||
|
||||
builder.Entity<ChapterPeople>()
|
||||
.HasOne(cp => cp.Chapter)
|
||||
.WithMany(c => c.People)
|
||||
.HasForeignKey(cp => cp.ChapterId);
|
||||
|
||||
builder.Entity<ChapterPeople>()
|
||||
.HasOne(cp => cp.Person)
|
||||
.WithMany(p => p.ChapterPeople)
|
||||
.HasForeignKey(cp => cp.PersonId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
|
||||
builder.Entity<SeriesMetadataPeople>()
|
||||
.HasKey(smp => new { smp.SeriesMetadataId, smp.PersonId, smp.Role });
|
||||
|
||||
builder.Entity<SeriesMetadataPeople>()
|
||||
.HasOne(smp => smp.SeriesMetadata)
|
||||
.WithMany(sm => sm.People)
|
||||
.HasForeignKey(smp => smp.SeriesMetadataId);
|
||||
|
||||
builder.Entity<SeriesMetadataPeople>()
|
||||
.HasOne(smp => smp.Person)
|
||||
.WithMany(p => p.SeriesMetadataPeople)
|
||||
.HasForeignKey(smp => smp.PersonId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
|
|
|
|||
51
API/Data/ManualMigrations/MigrateLowestSeriesFolderPath2.cs
Normal file
51
API/Data/ManualMigrations/MigrateLowestSeriesFolderPath2.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.8.3 still had a bug around LowestSeriesPath. This resets it for all users.
|
||||
/// </summary>
|
||||
public static class MigrateLowestSeriesFolderPath2
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLowestSeriesFolderPath2"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateLowestSeriesFolderPath2 migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var series = await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)).ToListAsync();
|
||||
foreach (var s in series)
|
||||
{
|
||||
s.LowestFolderPath = string.Empty;
|
||||
unitOfWork.SeriesRepository.Update(s);
|
||||
}
|
||||
|
||||
// Save changes after processing all series
|
||||
if (dataContext.ChangeTracker.HasChanges())
|
||||
{
|
||||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateLowestSeriesFolderPath2",
|
||||
ProductVersion = BuildInfo.Version.ToString(),
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
logger.LogCritical(
|
||||
"Running MigrateLowestSeriesFolderPath2 migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
3064
API/Data/Migrations/20240704144224_PersonFields.Designer.cs
generated
Normal file
3064
API/Data/Migrations/20240704144224_PersonFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
91
API/Data/Migrations/20240704144224_PersonFields.cs
Normal file
91
API/Data/Migrations/20240704144224_PersonFields.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PersonFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AniListId",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Asin",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CoverImageLocked",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Description",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "HardcoverId",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "MalId",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AniListId",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Asin",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImage",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImageLocked",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Description",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HardcoverId",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MalId",
|
||||
table: "Person");
|
||||
}
|
||||
}
|
||||
}
|
||||
3170
API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs
generated
Normal file
3170
API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
159
API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs
Normal file
159
API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PeopleOverhaulPart1 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChapterPerson");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PersonSeriesMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Role",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChapterPeople",
|
||||
columns: table => new
|
||||
{
|
||||
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
PersonId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Role = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChapterPeople", x => new { x.ChapterId, x.PersonId, x.Role });
|
||||
table.ForeignKey(
|
||||
name: "FK_ChapterPeople_Chapter_ChapterId",
|
||||
column: x => x.ChapterId,
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChapterPeople_Person_PersonId",
|
||||
column: x => x.PersonId,
|
||||
principalTable: "Person",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesMetadataPeople",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesMetadataId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
PersonId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Role = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesMetadataPeople", x => new { x.SeriesMetadataId, x.PersonId, x.Role });
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesMetadataPeople_Person_PersonId",
|
||||
column: x => x.PersonId,
|
||||
principalTable: "Person",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesMetadataPeople_SeriesMetadata_SeriesMetadataId",
|
||||
column: x => x.SeriesMetadataId,
|
||||
principalTable: "SeriesMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChapterPeople_PersonId",
|
||||
table: "ChapterPeople",
|
||||
column: "PersonId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesMetadataPeople_PersonId",
|
||||
table: "SeriesMetadataPeople",
|
||||
column: "PersonId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChapterPeople");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesMetadataPeople");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Role",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChapterPerson",
|
||||
columns: table => new
|
||||
{
|
||||
ChapterMetadatasId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
PeopleId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChapterPerson", x => new { x.ChapterMetadatasId, x.PeopleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ChapterPerson_Chapter_ChapterMetadatasId",
|
||||
column: x => x.ChapterMetadatasId,
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChapterPerson_Person_PeopleId",
|
||||
column: x => x.PeopleId,
|
||||
principalTable: "Person",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PersonSeriesMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
PeopleId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesMetadatasId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PersonSeriesMetadata", x => new { x.PeopleId, x.SeriesMetadatasId });
|
||||
table.ForeignKey(
|
||||
name: "FK_PersonSeriesMetadata_Person_PeopleId",
|
||||
column: x => x.PeopleId,
|
||||
principalTable: "Person",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_PersonSeriesMetadata_SeriesMetadata_SeriesMetadatasId",
|
||||
column: x => x.SeriesMetadatasId,
|
||||
principalTable: "SeriesMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChapterPerson_PeopleId",
|
||||
table: "ChapterPerson",
|
||||
column: "PeopleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersonSeriesMetadata_SeriesMetadatasId",
|
||||
table: "PersonSeriesMetadata",
|
||||
column: "SeriesMetadatasId");
|
||||
}
|
||||
}
|
||||
}
|
||||
3182
API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs
generated
Normal file
3182
API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
59
API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs
Normal file
59
API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PeopleOverhaulPart2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CoverImageLocked",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PrimaryColor",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecondaryColor",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImage",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImageLocked",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PrimaryColor",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecondaryColor",
|
||||
table: "Person");
|
||||
}
|
||||
}
|
||||
}
|
||||
3197
API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs
generated
Normal file
3197
API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
70
API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs
Normal file
70
API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PeopleOverhaulPart3 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AniListId",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Asin",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Description",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "HardcoverId",
|
||||
table: "Person",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "MalId",
|
||||
table: "Person",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AniListId",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Asin",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Description",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HardcoverId",
|
||||
table: "Person");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MalId",
|
||||
table: "Person");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -901,6 +901,24 @@ namespace API.Data.Migrations
|
|||
b.ToTable("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ChapterPeople", b =>
|
||||
{
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PersonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ChapterId", "PersonId", "Role");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("ChapterPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1531,14 +1549,38 @@ namespace API.Data.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AniListId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Asin")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HardcoverId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("MalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
|
@ -1903,6 +1945,24 @@ namespace API.Data.Migrations
|
|||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b =>
|
||||
{
|
||||
b.Property<int>("SeriesMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PersonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("SeriesMetadataId", "PersonId", "Role");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("SeriesMetadataPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
{
|
||||
b.Property<int>("Key")
|
||||
|
|
@ -2149,21 +2209,6 @@ namespace API.Data.Migrations
|
|||
b.ToTable("ChapterGenre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterPerson", b =>
|
||||
{
|
||||
b.Property<int>("ChapterMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PeopleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ChapterMetadatasId", "PeopleId");
|
||||
|
||||
b.HasIndex("PeopleId");
|
||||
|
||||
b.ToTable("ChapterPerson");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
{
|
||||
b.Property<int>("ChaptersId")
|
||||
|
|
@ -2338,21 +2383,6 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PersonSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("PeopleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("PeopleId", "SeriesMetadatasId");
|
||||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("PersonSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
{
|
||||
b.Property<int>("SeriesMetadatasId")
|
||||
|
|
@ -2600,6 +2630,25 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ChapterPeople", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany("People")
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Person", "Person")
|
||||
.WithMany("ChapterPeople")
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Device", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -2827,6 +2876,25 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Person", "Person")
|
||||
.WithMany("SeriesMetadataPeople")
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata")
|
||||
.WithMany("People")
|
||||
.HasForeignKey("SeriesMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Person");
|
||||
|
||||
b.Navigation("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
|
|
@ -2883,21 +2951,6 @@ namespace API.Data.Migrations
|
|||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterPerson", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Person", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PeopleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", null)
|
||||
|
|
@ -3024,21 +3077,6 @@ namespace API.Data.Migrations
|
|||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PersonSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Person", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PeopleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.SeriesMetadata", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Metadata.SeriesMetadata", null)
|
||||
|
|
@ -3096,6 +3134,8 @@ namespace API.Data.Migrations
|
|||
{
|
||||
b.Navigation("Files");
|
||||
|
||||
b.Navigation("People");
|
||||
|
||||
b.Navigation("UserProgress");
|
||||
});
|
||||
|
||||
|
|
@ -3110,6 +3150,18 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||
{
|
||||
b.Navigation("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
{
|
||||
b.Navigation("ChapterPeople");
|
||||
|
||||
b.Navigation("SeriesMetadataPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public interface ICollectionTagRepository
|
|||
Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None);
|
||||
Task<IList<AppUserCollection>> GetAllCollectionsForSyncing(DateTime expirationTime);
|
||||
}
|
||||
|
||||
public class CollectionTagRepository : ICollectionTagRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
|
|
@ -195,8 +196,10 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.Where(t => t.Id == tag.Id)
|
||||
.SelectMany(uc => uc.Items.Select(s => s.Metadata))
|
||||
.Select(sm => sm.AgeRating)
|
||||
.MaxAsync();
|
||||
tag.AgeRating = maxAgeRating;
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +222,6 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.AppUserCollection
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
.Where(r => EF.Functions.Like(r.Name, series.Name) ||
|
||||
EF.Functions.Like(r.Name, series.LocalizedName))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var rec in recMatches)
|
||||
{
|
||||
rec.SeriesId = series.Id;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using API.DTOs.Metadata;
|
|||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -24,6 +25,7 @@ public interface IGenreRepository
|
|||
Task<int> GetCountAsync();
|
||||
Task<GenreTagDto> GetRandomGenre();
|
||||
Task<GenreTagDto> GetGenreById(int id);
|
||||
Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames);
|
||||
}
|
||||
|
||||
public class GenreRepository : IGenreRepository
|
||||
|
|
@ -133,4 +135,33 @@ public class GenreRepository : IGenreRepository
|
|||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all genres that are not already present in the system.
|
||||
/// Normalizes genres for lookup, but returns non-normalized names for creation.
|
||||
/// </summary>
|
||||
/// <param name="genreNames">The list of genre names (non-normalized).</param>
|
||||
/// <returns>A list of genre names that do not exist in the system.</returns>
|
||||
public async Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames)
|
||||
{
|
||||
// Group the genres by their normalized names, keeping track of the original names
|
||||
var normalizedToOriginalMap = genreNames
|
||||
.Distinct()
|
||||
.GroupBy(Parser.Normalize)
|
||||
.ToDictionary(group => group.Key, group => group.First()); // Take the first original name for each normalized name
|
||||
|
||||
var normalizedGenreNames = normalizedToOriginalMap.Keys.ToList();
|
||||
|
||||
// Query the database for existing genres using the normalized names
|
||||
var existingGenres = await _context.Genre
|
||||
.Where(g => normalizedGenreNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field
|
||||
.Select(g => g.NormalizedTitle)
|
||||
.ToListAsync();
|
||||
|
||||
// Find the normalized genres that do not exist in the database
|
||||
var missingGenres = normalizedGenreNames.Except(existingGenres).ToList();
|
||||
|
||||
// Return the original non-normalized genres for the missing ones
|
||||
return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
|
|
@ -6,6 +8,7 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -15,7 +18,12 @@ namespace API.Data.Repositories;
|
|||
public interface IPersonRepository
|
||||
{
|
||||
void Attach(Person person);
|
||||
void Attach(IEnumerable<Person> person);
|
||||
void Remove(Person person);
|
||||
void Remove(ChapterPeople person);
|
||||
void Remove(SeriesMetadataPeople person);
|
||||
void Update(Person person);
|
||||
|
||||
Task<IList<Person>> GetAllPeople();
|
||||
Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId);
|
||||
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
|
||||
|
|
@ -23,7 +31,17 @@ public interface IPersonRepository
|
|||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null);
|
||||
Task<int> GetCountAsync();
|
||||
|
||||
Task<IList<Person>> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable<string> normalizeNames);
|
||||
Task<string> GetCoverImageAsync(int personId);
|
||||
Task<string?> GetCoverImageByNameAsync(string name);
|
||||
Task<IEnumerable<PersonRole>> GetRolesForPersonByName(string name, int userId);
|
||||
Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, UserParams userParams);
|
||||
Task<Person?> GetPersonById(int personId);
|
||||
Task<PersonDto?> GetPersonDtoByName(string name, int userId);
|
||||
Task<Person> GetPersonByName(string name);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
||||
Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role);
|
||||
Task<IList<Person>> GetPeopleByNames(List<string> normalizedNames);
|
||||
}
|
||||
|
||||
public class PersonRepository : IPersonRepository
|
||||
|
|
@ -42,17 +60,37 @@ public class PersonRepository : IPersonRepository
|
|||
_context.Person.Attach(person);
|
||||
}
|
||||
|
||||
public void Attach(IEnumerable<Person> person)
|
||||
{
|
||||
_context.Person.AttachRange(person);
|
||||
}
|
||||
|
||||
public void Remove(Person person)
|
||||
{
|
||||
_context.Person.Remove(person);
|
||||
}
|
||||
|
||||
public void Remove(ChapterPeople person)
|
||||
{
|
||||
_context.ChapterPeople.Remove(person);
|
||||
}
|
||||
|
||||
public void Remove(SeriesMetadataPeople person)
|
||||
{
|
||||
_context.SeriesMetadataPeople.Remove(person);
|
||||
}
|
||||
|
||||
public void Update(Person person)
|
||||
{
|
||||
_context.Person.Update(person);
|
||||
}
|
||||
|
||||
public async Task RemoveAllPeopleNoLongerAssociated()
|
||||
{
|
||||
var peopleWithNoConnections = await _context.Person
|
||||
.Include(p => p.SeriesMetadatas)
|
||||
.Include(p => p.ChapterMetadatas)
|
||||
.Where(p => p.SeriesMetadatas.Count == 0 && p.ChapterMetadatas.Count == 0)
|
||||
.Include(p => p.SeriesMetadataPeople)
|
||||
.Include(p => p.ChapterPeople)
|
||||
.Where(p => p.SeriesMetadataPeople.Count == 0 && p.ChapterPeople.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -61,6 +99,7 @@ public class PersonRepository : IPersonRepository
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
|
@ -74,7 +113,7 @@ public class PersonRepository : IPersonRepository
|
|||
return await _context.Series
|
||||
.Where(s => userLibs.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(s => s.Metadata.People)
|
||||
.SelectMany(s => s.Metadata.People.Select(p => p.Person))
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Name)
|
||||
.AsNoTracking()
|
||||
|
|
@ -88,13 +127,124 @@ public class PersonRepository : IPersonRepository
|
|||
return await _context.Person.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Person>> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable<string> normalizeNames)
|
||||
public async Task<string> GetCoverImageAsync(int personId)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(p => p.Role == role && normalizeNames.Contains(p.NormalizedName))
|
||||
.Where(c => c.Id == personId)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCoverImageByNameAsync(string name)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
return await _context.Person
|
||||
.Where(c => c.NormalizedName == normalized)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PersonRole>> GetRolesForPersonByName(string name, int userId)
|
||||
{
|
||||
// TODO: This will need to check both series and chapters (in cases where komf only updates series)
|
||||
var normalized = name.ToNormalized();
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.NormalizedName == normalized)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(p => p.ChapterPeople.Select(cp => cp.Role))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, UserParams userParams)
|
||||
{
|
||||
List<PersonRole> roles = [PersonRole.Writer, PersonRole.CoverArtist];
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var query = _context.Person
|
||||
.Where(p => p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role)))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.Select(p => new BrowsePersonDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
SeriesCount = p.SeriesMetadataPeople
|
||||
.Where(smp => roles.Contains(smp.Role))
|
||||
.Select(smp => smp.SeriesMetadata.SeriesId)
|
||||
.Distinct()
|
||||
.Count(),
|
||||
IssueCount = p.ChapterPeople
|
||||
.Where(cp => roles.Contains(cp.Role))
|
||||
.Select(cp => cp.Chapter.Id)
|
||||
.Distinct()
|
||||
.Count()
|
||||
})
|
||||
.OrderBy(p => p.Name);
|
||||
|
||||
return await PagedList<BrowsePersonDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<Person?> GetPersonById(int personId)
|
||||
{
|
||||
return await _context.Person.Where(p => p.Id == personId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<PersonDto> GetPersonDtoByName(string name, int userId)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.NormalizedName == normalized)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Person> GetPersonByName(string name)
|
||||
{
|
||||
return await _context.Person.FirstOrDefaultAsync(p => p.NormalizedName == name.ToNormalized());
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.SelectMany(p => p.SeriesMetadataPeople)
|
||||
.Select(smp => smp.SeriesMetadata)
|
||||
.Select(sm => sm.Series)
|
||||
.Distinct()
|
||||
.OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating)
|
||||
.Take(20)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.ChapterPeople
|
||||
.Where(cp => cp.PersonId == personId && cp.Role == role)
|
||||
.Select(cp => cp.Chapter)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.OrderBy(ch => ch.SortOrder)
|
||||
.Take(20)
|
||||
.ProjectTo<StandaloneChapterDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Person>> GetPeopleByNames(List<string> normalizedNames)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(p => normalizedNames.Contains(p.NormalizedName))
|
||||
.OrderBy(p => p.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Person>> GetAllPeople()
|
||||
{
|
||||
|
|
@ -106,7 +256,7 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
return await _context.Person
|
||||
.OrderBy(p => p.Name)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
|
|
@ -117,8 +267,9 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.Role == role)
|
||||
.Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters
|
||||
.OrderBy(p => p.Name)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
|
|
|
|||
|
|
@ -122,8 +122,10 @@ public class ReadingListRepository : IReadingListRepository
|
|||
{
|
||||
return _context.ReadingListItem
|
||||
.Where(item => item.ReadingListId == readingListId)
|
||||
.SelectMany(item => item.Chapter.People.Where(p => p.Role == PersonRole.Character))
|
||||
.OrderBy(p => p.NormalizedName)
|
||||
.SelectMany(item => item.Chapter.People)
|
||||
.Where(p => p.Role == PersonRole.Character)
|
||||
.OrderBy(p => p.Person.NormalizedName)
|
||||
.Select(p => p.Person)
|
||||
.Distinct()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.AsEnumerable();
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ public enum SeriesIncludes
|
|||
{
|
||||
None = 1,
|
||||
Volumes = 2,
|
||||
/// <summary>
|
||||
/// This will include all necessary includes
|
||||
/// </summary>
|
||||
Metadata = 4,
|
||||
Related = 8,
|
||||
Library = 16,
|
||||
|
|
@ -51,8 +54,7 @@ public enum SeriesIncludes
|
|||
ExternalReviews = 64,
|
||||
ExternalRatings = 128,
|
||||
ExternalRecommendations = 256,
|
||||
ExternalMetadata = 512
|
||||
|
||||
ExternalMetadata = 512,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -138,7 +140,7 @@ public interface ISeriesRepository
|
|||
Task<IList<Series>> GetWantToReadForUserAsync(int userId);
|
||||
Task<bool> IsSeriesInWantToRead(int userId, int seriesId);
|
||||
Task<Series?> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<Series?> GetSeriesThatContainsLowestFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<Series?> GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
|
||||
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
||||
|
|
@ -363,11 +365,11 @@ public class SeriesRepository : ISeriesRepository
|
|||
var searchQueryNormalized = searchQuery.ToNormalized();
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var seriesIds = _context.Series
|
||||
var seriesIds = await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Select(s => s.Id)
|
||||
.ToList();
|
||||
.ToListAsync();
|
||||
|
||||
result.Libraries = await _context.Library
|
||||
.Search(searchQuery, userId, libraryIds)
|
||||
|
|
@ -440,6 +442,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SearchPeople(searchQuery, seriesIds)
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.NormalizedName)
|
||||
.Distinct()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -532,14 +535,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Series?> GetSeriesByIdForUserAsync(int seriesId, int userId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.Includes(includes)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Full Series including all external links
|
||||
/// </summary>
|
||||
|
|
@ -661,6 +656,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(m => m.Genres.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.Tags.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.People)
|
||||
.ThenInclude(p => p.Person)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesMetadataDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
|
|
@ -1273,7 +1269,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
var query = sQuery
|
||||
.WhereIf(hasGenresFilter, s => s.Metadata.Genres.Any(g => filter.Genres.Contains(g.Id)))
|
||||
.WhereIf(hasPeopleFilter, s => s.Metadata.People.Any(p => allPeopleIds.Contains(p.Id)))
|
||||
.WhereIf(hasPeopleFilter, s => s.Metadata.People.Any(p => allPeopleIds.Contains(p.PersonId)))
|
||||
.WhereIf(hasCollectionTagFilter,
|
||||
s => s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id)))
|
||||
.WhereIf(hasRatingFilter, s => s.Ratings.Any(r => r.Rating >= filter.Rating && r.AppUserId == userId))
|
||||
|
|
@ -1302,6 +1298,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(m => m.Genres.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.Tags.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.People)
|
||||
.ThenInclude(p => p.Person)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesMetadataDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
|
|
@ -1606,9 +1603,24 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Series?> GetSeriesThatContainsLowestFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None)
|
||||
public async Task<Series?> GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None)
|
||||
{
|
||||
var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder);
|
||||
// Check if the path ends with a file (has a file extension)
|
||||
string directoryPath;
|
||||
if (Path.HasExtension(path))
|
||||
{
|
||||
// Remove the file part and get the directory path
|
||||
directoryPath = Path.GetDirectoryName(path);
|
||||
if (string.IsNullOrEmpty(directoryPath)) return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use the path as is if it doesn't end with a file
|
||||
directoryPath = path;
|
||||
}
|
||||
|
||||
// Normalize the directory path
|
||||
var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(directoryPath);
|
||||
if (string.IsNullOrEmpty(normalized)) return null;
|
||||
|
||||
normalized = normalized.TrimEnd('/');
|
||||
|
|
@ -1672,6 +1684,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
.ThenInclude(p => p.Person)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
|
|
@ -1682,6 +1695,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(cm => cm.People)
|
||||
.ThenInclude(p => p.Person)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
|
|
@ -1697,6 +1711,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
.AsSplitQuery();
|
||||
return query.SingleOrDefaultAsync();
|
||||
|
||||
#nullable enable
|
||||
}
|
||||
|
||||
|
|
@ -1705,6 +1720,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
var normalizedSeries = seriesName.ToNormalized();
|
||||
var normalizedLocalized = localizedName.ToNormalized();
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => formats.Contains(s.Format))
|
||||
|
|
@ -1749,45 +1765,36 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// <param name="libraryId"></param>
|
||||
public async Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId)
|
||||
{
|
||||
if (seenSeries.Count == 0) return Array.Empty<Series>();
|
||||
if (!seenSeries.Any()) return Array.Empty<Series>();
|
||||
|
||||
// Get all series from DB in one go, based on libraryId
|
||||
var dbSeries = await _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.ToListAsync();
|
||||
|
||||
// Get a set of matching series ids for the given parsedSeries
|
||||
var ids = new HashSet<int>();
|
||||
|
||||
var ids = new List<int>();
|
||||
foreach (var parsedSeries in seenSeries)
|
||||
{
|
||||
try
|
||||
var matchingSeries = dbSeries
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName)
|
||||
.OrderBy(s => s.Id) // Sort to handle potential duplicates
|
||||
.ToList();
|
||||
|
||||
// Prefer the first match or handle duplicates by choosing the last one
|
||||
if (matchingSeries.Any())
|
||||
{
|
||||
var seriesId = await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
if (seriesId > 0)
|
||||
{
|
||||
ids.Add(seriesId);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// This is due to v0.5.6 introducing bugs where we could have multiple series get duplicated and no way to delete them
|
||||
// This here will delete the 2nd one as the first is the one to likely be used.
|
||||
var sId = await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.OrderBy(s => s)
|
||||
.LastAsync();
|
||||
if (sId > 0)
|
||||
{
|
||||
ids.Add(sId);
|
||||
}
|
||||
ids.Add(matchingSeries.Last().Id);
|
||||
}
|
||||
}
|
||||
|
||||
var seriesToRemove = await _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
// Filter out series that are not in the seenSeries
|
||||
var seriesToRemove = dbSeries
|
||||
.Where(s => !ids.Contains(s.Id))
|
||||
.ToListAsync();
|
||||
.ToList();
|
||||
|
||||
// Remove series in bulk
|
||||
_context.Series.RemoveRange(seriesToRemove);
|
||||
|
||||
return seriesToRemove;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using API.DTOs.Metadata;
|
|||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -20,6 +21,7 @@ public interface ITagRepository
|
|||
Task<IList<TagDto>> GetAllTagDtosAsync(int userId);
|
||||
Task RemoveAllTagNoLongerAssociated();
|
||||
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(int userId, IList<int>? libraryIds = null);
|
||||
Task<List<string>> GetAllTagsNotInListAsync(ICollection<string> tags);
|
||||
}
|
||||
|
||||
public class TagRepository : ITagRepository
|
||||
|
|
@ -79,6 +81,28 @@ public class TagRepository : ITagRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetAllTagsNotInListAsync(ICollection<string> tags)
|
||||
{
|
||||
// Create a dictionary mapping normalized names to non-normalized names
|
||||
var normalizedToOriginalMap = tags.Distinct()
|
||||
.GroupBy(Parser.Normalize)
|
||||
.ToDictionary(group => group.Key, group => group.First());
|
||||
|
||||
var normalizedTagNames = normalizedToOriginalMap.Keys.ToList();
|
||||
|
||||
// Query the database for existing genres using the normalized names
|
||||
var existingTags = await _context.Tag
|
||||
.Where(g => normalizedTagNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field
|
||||
.Select(g => g.NormalizedTitle)
|
||||
.ToListAsync();
|
||||
|
||||
// Find the normalized genres that do not exist in the database
|
||||
var missingTags = normalizedTagNames.Except(existingTags).ToList();
|
||||
|
||||
// Return the original non-normalized genres for the missing ones
|
||||
return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
|
||||
}
|
||||
|
||||
public async Task<IList<Tag>> GetAllTagsAsync()
|
||||
{
|
||||
return await _context.Tag.ToListAsync();
|
||||
|
|
|
|||
|
|
@ -114,6 +114,14 @@ public static class Seed
|
|||
Order = 5,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "browse-authors",
|
||||
StreamType = SideNavStreamType.BrowseAuthors,
|
||||
Order = 6,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -183,10 +191,10 @@ public static class Seed
|
|||
var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams);
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
if (user.SideNavStreams.Count != 0) continue;
|
||||
user.SideNavStreams ??= new List<AppUserSideNavStream>();
|
||||
foreach (var defaultStream in DefaultSideNavStreams)
|
||||
{
|
||||
if (user.SideNavStreams.Any(s => s.Name == defaultStream.Name && s.StreamType == defaultStream.StreamType)) continue;
|
||||
var newStream = new AppUserSideNavStream()
|
||||
{
|
||||
Name = defaultStream.Name,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ namespace API.Data;
|
|||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
DataContext DataContext { get; }
|
||||
ISeriesRepository SeriesRepository { get; }
|
||||
IUserRepository UserRepository { get; }
|
||||
ILibraryRepository LibraryRepository { get; }
|
||||
|
|
@ -36,6 +37,7 @@ public interface IUnitOfWork
|
|||
bool HasChanges();
|
||||
Task<bool> RollbackAsync();
|
||||
}
|
||||
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
|
|
@ -47,33 +49,57 @@ public class UnitOfWork : IUnitOfWork
|
|||
_context = context;
|
||||
_mapper = mapper;
|
||||
_userManager = userManager;
|
||||
|
||||
SeriesRepository = new SeriesRepository(_context, _mapper, _userManager);
|
||||
UserRepository = new UserRepository(_context, _userManager, _mapper);
|
||||
LibraryRepository = new LibraryRepository(_context, _mapper);
|
||||
VolumeRepository = new VolumeRepository(_context, _mapper);
|
||||
SettingsRepository = new SettingsRepository(_context, _mapper);
|
||||
AppUserProgressRepository = new AppUserProgressRepository(_context, _mapper);
|
||||
CollectionTagRepository = new CollectionTagRepository(_context, _mapper);
|
||||
ChapterRepository = new ChapterRepository(_context, _mapper);
|
||||
ReadingListRepository = new ReadingListRepository(_context, _mapper);
|
||||
SeriesMetadataRepository = new SeriesMetadataRepository(_context);
|
||||
PersonRepository = new PersonRepository(_context, _mapper);
|
||||
GenreRepository = new GenreRepository(_context, _mapper);
|
||||
TagRepository = new TagRepository(_context, _mapper);
|
||||
SiteThemeRepository = new SiteThemeRepository(_context, _mapper);
|
||||
MangaFileRepository = new MangaFileRepository(_context);
|
||||
DeviceRepository = new DeviceRepository(_context, _mapper);
|
||||
MediaErrorRepository = new MediaErrorRepository(_context, _mapper);
|
||||
ScrobbleRepository = new ScrobbleRepository(_context, _mapper);
|
||||
UserTableOfContentRepository = new UserTableOfContentRepository(_context, _mapper);
|
||||
AppUserSmartFilterRepository = new AppUserSmartFilterRepository(_context, _mapper);
|
||||
AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper);
|
||||
ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper);
|
||||
}
|
||||
|
||||
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _userManager);
|
||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper);
|
||||
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
||||
|
||||
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
||||
|
||||
public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper);
|
||||
|
||||
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context, _mapper);
|
||||
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
|
||||
public IChapterRepository ChapterRepository => new ChapterRepository(_context, _mapper);
|
||||
public IReadingListRepository ReadingListRepository => new ReadingListRepository(_context, _mapper);
|
||||
public ISeriesMetadataRepository SeriesMetadataRepository => new SeriesMetadataRepository(_context);
|
||||
public IPersonRepository PersonRepository => new PersonRepository(_context, _mapper);
|
||||
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
|
||||
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
|
||||
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
|
||||
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context);
|
||||
public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper);
|
||||
public IMediaErrorRepository MediaErrorRepository => new MediaErrorRepository(_context, _mapper);
|
||||
public IScrobbleRepository ScrobbleRepository => new ScrobbleRepository(_context, _mapper);
|
||||
public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper);
|
||||
public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper);
|
||||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper);
|
||||
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository => new ExternalSeriesMetadataRepository(_context, _mapper);
|
||||
/// <summary>
|
||||
/// This is here for Scanner only. Don't use otherwise.
|
||||
/// </summary>
|
||||
public DataContext DataContext => _context;
|
||||
public ISeriesRepository SeriesRepository { get; }
|
||||
public IUserRepository UserRepository { get; }
|
||||
public ILibraryRepository LibraryRepository { get; }
|
||||
public IVolumeRepository VolumeRepository { get; }
|
||||
public ISettingsRepository SettingsRepository { get; }
|
||||
public IAppUserProgressRepository AppUserProgressRepository { get; }
|
||||
public ICollectionTagRepository CollectionTagRepository { get; }
|
||||
public IChapterRepository ChapterRepository { get; }
|
||||
public IReadingListRepository ReadingListRepository { get; }
|
||||
public ISeriesMetadataRepository SeriesMetadataRepository { get; }
|
||||
public IPersonRepository PersonRepository { get; }
|
||||
public IGenreRepository GenreRepository { get; }
|
||||
public ITagRepository TagRepository { get; }
|
||||
public ISiteThemeRepository SiteThemeRepository { get; }
|
||||
public IMangaFileRepository MangaFileRepository { get; }
|
||||
public IDeviceRepository DeviceRepository { get; }
|
||||
public IMediaErrorRepository MediaErrorRepository { get; }
|
||||
public IScrobbleRepository ScrobbleRepository { get; }
|
||||
public IUserTableOfContentRepository UserTableOfContentRepository { get; }
|
||||
public IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue