diff --git a/API/Migrations/Library/20250703191925_Initial.Designer.cs b/API/Migrations/Library/20250703191925_Initial.Designer.cs
new file mode 100644
index 0000000..85754b7
--- /dev/null
+++ b/API/Migrations/Library/20250703191925_Initial.Designer.cs
@@ -0,0 +1,70 @@
+//
+using API.Schema.LibraryContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Library
+{
+ [DbContext(typeof(LibraryContext))]
+ [Migration("20250703191925_Initial")]
+ partial class Initial
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Auth")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("BaseUrl")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryType")
+ .HasColumnType("smallint");
+
+ b.HasKey("Key");
+
+ b.ToTable("LibraryConnectors");
+
+ b.HasDiscriminator("LibraryType");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
+ {
+ b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
+
+ b.HasDiscriminator().HasValue((byte)1);
+ });
+
+ modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
+ {
+ b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
+
+ b.HasDiscriminator().HasValue((byte)0);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/Library/20250703191925_Initial.cs b/API/Migrations/Library/20250703191925_Initial.cs
new file mode 100644
index 0000000..6de0f48
--- /dev/null
+++ b/API/Migrations/Library/20250703191925_Initial.cs
@@ -0,0 +1,35 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.Library
+{
+ ///
+ public partial class Initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "LibraryConnectors",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ LibraryType = table.Column(type: "smallint", nullable: false),
+ BaseUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ Auth = table.Column(type: "character varying(256)", maxLength: 256, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_LibraryConnectors", x => x.Key);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "LibraryConnectors");
+ }
+ }
+}
diff --git a/API/Migrations/Library/LibraryContextModelSnapshot.cs b/API/Migrations/Library/LibraryContextModelSnapshot.cs
new file mode 100644
index 0000000..8f2d077
--- /dev/null
+++ b/API/Migrations/Library/LibraryContextModelSnapshot.cs
@@ -0,0 +1,67 @@
+//
+using API.Schema.LibraryContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Library
+{
+ [DbContext(typeof(LibraryContext))]
+ partial class LibraryContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Auth")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("BaseUrl")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryType")
+ .HasColumnType("smallint");
+
+ b.HasKey("Key");
+
+ b.ToTable("LibraryConnectors");
+
+ b.HasDiscriminator("LibraryType");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
+ {
+ b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
+
+ b.HasDiscriminator().HasValue((byte)1);
+ });
+
+ modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
+ {
+ b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
+
+ b.HasDiscriminator().HasValue((byte)0);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/Manga/20250703192023_Initial.Designer.cs b/API/Migrations/Manga/20250703192023_Initial.Designer.cs
new file mode 100644
index 0000000..b728b26
--- /dev/null
+++ b/API/Migrations/Manga/20250703192023_Initial.Designer.cs
@@ -0,0 +1,547 @@
+//
+using API.Schema.MangaContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Manga
+{
+ [DbContext(typeof(MangaContext))]
+ [Migration("20250703192023_Initial")]
+ partial class Initial
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("AuthorName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("Key");
+
+ b.ToTable("Authors");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("ChapterNumber")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("Downloaded")
+ .HasColumnType("boolean");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ParentMangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Title")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("VolumeNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Key");
+
+ b.HasIndex("ParentMangaId");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("BasePath")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Key");
+
+ b.ToTable("FileLibraries");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("CoverFileNameInCache")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("CoverUrl")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("IgnoreChaptersBefore")
+ .HasColumnType("real");
+
+ b.Property("LibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("OriginalLanguage")
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("ReleaseStatus")
+ .HasColumnType("smallint");
+
+ b.Property("Year")
+ .HasColumnType("bigint");
+
+ b.HasKey("Key");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("Mangas");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("ObjId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("UseForDownload")
+ .HasColumnType("boolean");
+
+ b.Property("WebsiteUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Key");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.HasIndex("ObjId");
+
+ b.ToTable("MangaConnectorToChapter");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("ObjId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("UseForDownload")
+ .HasColumnType("boolean");
+
+ b.Property("WebsiteUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Key");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.HasIndex("ObjId");
+
+ b.ToTable("MangaConnectorToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.PrimitiveCollection("BaseUris")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("text[]");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("IconUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.PrimitiveCollection("SupportedLanguages")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("text[]");
+
+ b.HasKey("Name");
+
+ b.ToTable("MangaConnectors");
+
+ b.HasDiscriminator("Name").HasValue("MangaConnector");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
+ {
+ b.Property("Tag")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("Tag");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
+ {
+ b.Property("MetadataFetcherName")
+ .HasColumnType("text");
+
+ b.Property("Identifier")
+ .HasColumnType("text");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("MetadataFetcherName", "Identifier");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("MetadataEntries");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("MetadataEntry")
+ .IsRequired()
+ .HasMaxLength(21)
+ .HasColumnType("character varying(21)");
+
+ b.HasKey("Name");
+
+ b.ToTable("MetadataFetcher");
+
+ b.HasDiscriminator("MetadataEntry").HasValue("MetadataFetcher");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.Property("AuthorIds")
+ .HasColumnType("text");
+
+ b.Property("MangaIds")
+ .HasColumnType("text");
+
+ b.HasKey("AuthorIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("AuthorToManga");
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.Property("MangaTagIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("text");
+
+ b.HasKey("MangaTagIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("MangaTagToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("ComickIo");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("Global");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("MangaDex");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
+
+ b.HasDiscriminator().HasValue("MyAnimeList");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
+ .WithMany("Chapters")
+ .HasForeignKey("ParentMangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ParentManga");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
+ .WithMany()
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
+ {
+ b1.Property("Key")
+ .HasColumnType("text");
+
+ b1.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b1.Property("MangaKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b1.Property("Title")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.HasKey("Key");
+
+ b1.HasIndex("MangaKey");
+
+ b1.ToTable("AltTitle");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaKey");
+ });
+
+ b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
+ {
+ b1.Property("Key")
+ .HasColumnType("text");
+
+ b1.Property("LinkProvider")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("MangaKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b1.HasKey("Key");
+
+ b1.HasIndex("MangaKey");
+
+ b1.ToTable("Link");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaKey");
+ });
+
+ b.Navigation("AltTitles");
+
+ b.Navigation("Library");
+
+ b.Navigation("Links");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
+ .WithMany("MangaConnectorIds")
+ .HasForeignKey("ObjId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("MangaConnector");
+
+ b.Navigation("Obj");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.Manga", "Obj")
+ .WithMany("MangaConnectorIds")
+ .HasForeignKey("ObjId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("MangaConnector");
+
+ b.Navigation("Obj");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
+ .WithMany()
+ .HasForeignKey("MetadataFetcherName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+
+ b.Navigation("MetadataFetcher");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Author", null)
+ .WithMany()
+ .HasForeignKey("AuthorIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.MangaTag", null)
+ .WithMany()
+ .HasForeignKey("MangaTagIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
+ {
+ b.Navigation("MangaConnectorIds");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("MangaConnectorIds");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/Manga/20250703192023_Initial.cs b/API/Migrations/Manga/20250703192023_Initial.cs
new file mode 100644
index 0000000..cb0a7e7
--- /dev/null
+++ b/API/Migrations/Manga/20250703192023_Initial.cs
@@ -0,0 +1,396 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.Manga
+{
+ ///
+ public partial class Initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Authors",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ AuthorName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Authors", x => x.Key);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "FileLibraries",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ BasePath = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ LibraryName = table.Column(type: "character varying(512)", maxLength: 512, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_FileLibraries", x => x.Key);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MangaConnectors",
+ columns: table => new
+ {
+ Name = table.Column(type: "character varying(32)", maxLength: 32, nullable: false),
+ SupportedLanguages = table.Column(type: "text[]", maxLength: 8, nullable: false),
+ IconUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false),
+ BaseUris = table.Column(type: "text[]", maxLength: 256, nullable: false),
+ Enabled = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MangaConnectors", x => x.Name);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MetadataFetcher",
+ columns: table => new
+ {
+ Name = table.Column(type: "text", nullable: false),
+ MetadataEntry = table.Column(type: "character varying(21)", maxLength: 21, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MetadataFetcher", x => x.Name);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Tags",
+ columns: table => new
+ {
+ Tag = table.Column(type: "character varying(64)", maxLength: 64, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Tags", x => x.Tag);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Mangas",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false),
+ Description = table.Column(type: "text", nullable: false),
+ CoverUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false),
+ ReleaseStatus = table.Column(type: "smallint", nullable: false),
+ LibraryId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true),
+ IgnoreChaptersBefore = table.Column(type: "real", nullable: false),
+ DirectoryName = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false),
+ CoverFileNameInCache = table.Column(type: "character varying(512)", maxLength: 512, nullable: true),
+ Year = table.Column(type: "bigint", nullable: true),
+ OriginalLanguage = table.Column(type: "character varying(8)", maxLength: 8, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Mangas", x => x.Key);
+ table.ForeignKey(
+ name: "FK_Mangas_FileLibraries_LibraryId",
+ column: x => x.LibraryId,
+ principalTable: "FileLibraries",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.SetNull);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AltTitle",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ Language = table.Column(type: "character varying(8)", maxLength: 8, nullable: false),
+ Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ MangaKey = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AltTitle", x => x.Key);
+ table.ForeignKey(
+ name: "FK_AltTitle_Mangas_MangaKey",
+ column: x => x.MangaKey,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AuthorToManga",
+ columns: table => new
+ {
+ AuthorIds = table.Column(type: "text", nullable: false),
+ MangaIds = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
+ table.ForeignKey(
+ name: "FK_AuthorToManga_Authors_AuthorIds",
+ column: x => x.AuthorIds,
+ principalTable: "Authors",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AuthorToManga_Mangas_MangaIds",
+ column: x => x.MangaIds,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Chapters",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ ParentMangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false),
+ VolumeNumber = table.Column(type: "integer", nullable: true),
+ ChapterNumber = table.Column(type: "character varying(10)", maxLength: 10, nullable: false),
+ Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ FileName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ Downloaded = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Chapters", x => x.Key);
+ table.ForeignKey(
+ name: "FK_Chapters_Mangas_ParentMangaId",
+ column: x => x.ParentMangaId,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Link",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ LinkProvider = table.Column(type: "character varying(64)", maxLength: 64, nullable: false),
+ LinkUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false),
+ MangaKey = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Link", x => x.Key);
+ table.ForeignKey(
+ name: "FK_Link_Mangas_MangaKey",
+ column: x => x.MangaKey,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MangaConnectorToManga",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ ObjId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false),
+ MangaConnectorName = table.Column(type: "character varying(32)", maxLength: 32, nullable: false),
+ IdOnConnectorSite = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ WebsiteUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true),
+ UseForDownload = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
+ table.ForeignKey(
+ name: "FK_MangaConnectorToManga_MangaConnectors_MangaConnectorName",
+ column: x => x.MangaConnectorName,
+ principalTable: "MangaConnectors",
+ principalColumn: "Name",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_MangaConnectorToManga_Mangas_ObjId",
+ column: x => x.ObjId,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MangaTagToManga",
+ columns: table => new
+ {
+ MangaTagIds = table.Column(type: "character varying(64)", nullable: false),
+ MangaIds = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
+ table.ForeignKey(
+ name: "FK_MangaTagToManga_Mangas_MangaIds",
+ column: x => x.MangaIds,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_MangaTagToManga_Tags_MangaTagIds",
+ column: x => x.MangaTagIds,
+ principalTable: "Tags",
+ principalColumn: "Tag",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MetadataEntries",
+ columns: table => new
+ {
+ MetadataFetcherName = table.Column(type: "text", nullable: false),
+ Identifier = table.Column(type: "text", nullable: false),
+ MangaId = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MetadataEntries", x => new { x.MetadataFetcherName, x.Identifier });
+ table.ForeignKey(
+ name: "FK_MetadataEntries_Mangas_MangaId",
+ column: x => x.MangaId,
+ principalTable: "Mangas",
+ principalColumn: "Key",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_MetadataEntries_MetadataFetcher_MetadataFetcherName",
+ column: x => x.MetadataFetcherName,
+ principalTable: "MetadataFetcher",
+ principalColumn: "Name",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MangaConnectorToChapter",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ ObjId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false),
+ MangaConnectorName = table.Column(type: "character varying(32)", maxLength: 32, nullable: false),
+ IdOnConnectorSite = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ WebsiteUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true),
+ UseForDownload = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MangaConnectorToChapter", x => x.Key);
+ table.ForeignKey(
+ name: "FK_MangaConnectorToChapter_Chapters_ObjId",
+ column: x => x.ObjId,
+ principalTable: "Chapters",
+ principalColumn: "Key");
+ table.ForeignKey(
+ name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
+ column: x => x.MangaConnectorName,
+ principalTable: "MangaConnectors",
+ principalColumn: "Name",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AltTitle_MangaKey",
+ table: "AltTitle",
+ column: "MangaKey");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AuthorToManga_MangaIds",
+ table: "AuthorToManga",
+ column: "MangaIds");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Chapters_ParentMangaId",
+ table: "Chapters",
+ column: "ParentMangaId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Link_MangaKey",
+ table: "Link",
+ column: "MangaKey");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaConnectorToChapter_MangaConnectorName",
+ table: "MangaConnectorToChapter",
+ column: "MangaConnectorName");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaConnectorToChapter_ObjId",
+ table: "MangaConnectorToChapter",
+ column: "ObjId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaConnectorToManga_MangaConnectorName",
+ table: "MangaConnectorToManga",
+ column: "MangaConnectorName");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaConnectorToManga_ObjId",
+ table: "MangaConnectorToManga",
+ column: "ObjId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Mangas_LibraryId",
+ table: "Mangas",
+ column: "LibraryId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaTagToManga_MangaIds",
+ table: "MangaTagToManga",
+ column: "MangaIds");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MetadataEntries_MangaId",
+ table: "MetadataEntries",
+ column: "MangaId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AltTitle");
+
+ migrationBuilder.DropTable(
+ name: "AuthorToManga");
+
+ migrationBuilder.DropTable(
+ name: "Link");
+
+ migrationBuilder.DropTable(
+ name: "MangaConnectorToChapter");
+
+ migrationBuilder.DropTable(
+ name: "MangaConnectorToManga");
+
+ migrationBuilder.DropTable(
+ name: "MangaTagToManga");
+
+ migrationBuilder.DropTable(
+ name: "MetadataEntries");
+
+ migrationBuilder.DropTable(
+ name: "Authors");
+
+ migrationBuilder.DropTable(
+ name: "Chapters");
+
+ migrationBuilder.DropTable(
+ name: "MangaConnectors");
+
+ migrationBuilder.DropTable(
+ name: "Tags");
+
+ migrationBuilder.DropTable(
+ name: "MetadataFetcher");
+
+ migrationBuilder.DropTable(
+ name: "Mangas");
+
+ migrationBuilder.DropTable(
+ name: "FileLibraries");
+ }
+ }
+}
diff --git a/API/Migrations/Manga/MangaContextModelSnapshot.cs b/API/Migrations/Manga/MangaContextModelSnapshot.cs
new file mode 100644
index 0000000..ddaad3b
--- /dev/null
+++ b/API/Migrations/Manga/MangaContextModelSnapshot.cs
@@ -0,0 +1,544 @@
+//
+using API.Schema.MangaContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Manga
+{
+ [DbContext(typeof(MangaContext))]
+ partial class MangaContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("AuthorName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("Key");
+
+ b.ToTable("Authors");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("ChapterNumber")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("Downloaded")
+ .HasColumnType("boolean");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ParentMangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Title")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("VolumeNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Key");
+
+ b.HasIndex("ParentMangaId");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("BasePath")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Key");
+
+ b.ToTable("FileLibraries");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("CoverFileNameInCache")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("CoverUrl")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("IgnoreChaptersBefore")
+ .HasColumnType("real");
+
+ b.Property("LibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("OriginalLanguage")
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("ReleaseStatus")
+ .HasColumnType("smallint");
+
+ b.Property("Year")
+ .HasColumnType("bigint");
+
+ b.HasKey("Key");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("Mangas");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("ObjId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("UseForDownload")
+ .HasColumnType("boolean");
+
+ b.Property("WebsiteUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Key");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.HasIndex("ObjId");
+
+ b.ToTable("MangaConnectorToChapter");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("ObjId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("UseForDownload")
+ .HasColumnType("boolean");
+
+ b.Property("WebsiteUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Key");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.HasIndex("ObjId");
+
+ b.ToTable("MangaConnectorToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.PrimitiveCollection("BaseUris")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("text[]");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("IconUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.PrimitiveCollection("SupportedLanguages")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("text[]");
+
+ b.HasKey("Name");
+
+ b.ToTable("MangaConnectors");
+
+ b.HasDiscriminator("Name").HasValue("MangaConnector");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
+ {
+ b.Property("Tag")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("Tag");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
+ {
+ b.Property("MetadataFetcherName")
+ .HasColumnType("text");
+
+ b.Property("Identifier")
+ .HasColumnType("text");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("MetadataFetcherName", "Identifier");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("MetadataEntries");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("MetadataEntry")
+ .IsRequired()
+ .HasMaxLength(21)
+ .HasColumnType("character varying(21)");
+
+ b.HasKey("Name");
+
+ b.ToTable("MetadataFetcher");
+
+ b.HasDiscriminator("MetadataEntry").HasValue("MetadataFetcher");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.Property("AuthorIds")
+ .HasColumnType("text");
+
+ b.Property("MangaIds")
+ .HasColumnType("text");
+
+ b.HasKey("AuthorIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("AuthorToManga");
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.Property("MangaTagIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("text");
+
+ b.HasKey("MangaTagIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("MangaTagToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("ComickIo");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("Global");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("MangaDex");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
+ {
+ b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
+
+ b.HasDiscriminator().HasValue("MyAnimeList");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
+ .WithMany("Chapters")
+ .HasForeignKey("ParentMangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ParentManga");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
+ .WithMany()
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
+ {
+ b1.Property("Key")
+ .HasColumnType("text");
+
+ b1.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b1.Property("MangaKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b1.Property("Title")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.HasKey("Key");
+
+ b1.HasIndex("MangaKey");
+
+ b1.ToTable("AltTitle");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaKey");
+ });
+
+ b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
+ {
+ b1.Property("Key")
+ .HasColumnType("text");
+
+ b1.Property("LinkProvider")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("MangaKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b1.HasKey("Key");
+
+ b1.HasIndex("MangaKey");
+
+ b1.ToTable("Link");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaKey");
+ });
+
+ b.Navigation("AltTitles");
+
+ b.Navigation("Library");
+
+ b.Navigation("Links");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
+ .WithMany("MangaConnectorIds")
+ .HasForeignKey("ObjId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("MangaConnector");
+
+ b.Navigation("Obj");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.Manga", "Obj")
+ .WithMany("MangaConnectorIds")
+ .HasForeignKey("ObjId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("MangaConnector");
+
+ b.Navigation("Obj");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
+ .WithMany()
+ .HasForeignKey("MetadataFetcherName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+
+ b.Navigation("MetadataFetcher");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Author", null)
+ .WithMany()
+ .HasForeignKey("AuthorIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.HasOne("API.Schema.MangaContext.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaContext.MangaTag", null)
+ .WithMany()
+ .HasForeignKey("MangaTagIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
+ {
+ b.Navigation("MangaConnectorIds");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("MangaConnectorIds");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/Notifications/20250703191820_Initial.Designer.cs b/API/Migrations/Notifications/20250703191820_Initial.Designer.cs
new file mode 100644
index 0000000..92d67e4
--- /dev/null
+++ b/API/Migrations/Notifications/20250703191820_Initial.Designer.cs
@@ -0,0 +1,91 @@
+//
+using System;
+using System.Collections.Generic;
+using API.Schema.NotificationsContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Notifications
+{
+ [DbContext(typeof(NotificationsContext))]
+ [Migration("20250703191820_Initial")]
+ partial class Initial
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsSent")
+ .HasColumnType("boolean");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Urgency")
+ .HasColumnType("smallint");
+
+ b.HasKey("Key");
+
+ b.ToTable("Notifications");
+ });
+
+ modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Body")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)");
+
+ b.Property>("Headers")
+ .IsRequired()
+ .HasColumnType("hstore");
+
+ b.Property("HttpMethod")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.HasKey("Name");
+
+ b.ToTable("NotificationConnectors");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/Notifications/20250703191820_Initial.cs b/API/Migrations/Notifications/20250703191820_Initial.cs
new file mode 100644
index 0000000..8a80fc3
--- /dev/null
+++ b/API/Migrations/Notifications/20250703191820_Initial.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.Notifications
+{
+ ///
+ public partial class Initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("Npgsql:PostgresExtension:hstore", ",,");
+
+ migrationBuilder.CreateTable(
+ name: "NotificationConnectors",
+ columns: table => new
+ {
+ Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false),
+ Url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false),
+ Headers = table.Column>(type: "hstore", nullable: false),
+ HttpMethod = table.Column(type: "character varying(8)", maxLength: 8, nullable: false),
+ Body = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Notifications",
+ columns: table => new
+ {
+ Key = table.Column(type: "text", nullable: false),
+ Urgency = table.Column(type: "smallint", nullable: false),
+ Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: false),
+ Message = table.Column(type: "character varying(512)", maxLength: 512, nullable: false),
+ Date = table.Column(type: "timestamp with time zone", nullable: false),
+ IsSent = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Notifications", x => x.Key);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "NotificationConnectors");
+
+ migrationBuilder.DropTable(
+ name: "Notifications");
+ }
+ }
+}
diff --git a/API/Migrations/Notifications/NotificationsContextModelSnapshot.cs b/API/Migrations/Notifications/NotificationsContextModelSnapshot.cs
new file mode 100644
index 0000000..ad8af14
--- /dev/null
+++ b/API/Migrations/Notifications/NotificationsContextModelSnapshot.cs
@@ -0,0 +1,88 @@
+//
+using System;
+using System.Collections.Generic;
+using API.Schema.NotificationsContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.Notifications
+{
+ [DbContext(typeof(NotificationsContext))]
+ partial class NotificationsContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsSent")
+ .HasColumnType("boolean");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Urgency")
+ .HasColumnType("smallint");
+
+ b.HasKey("Key");
+
+ b.ToTable("Notifications");
+ });
+
+ modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Body")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)");
+
+ b.Property>("Headers")
+ .IsRequired()
+ .HasColumnType("hstore");
+
+ b.Property("HttpMethod")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.HasKey("Name");
+
+ b.ToTable("NotificationConnectors");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Program.cs b/API/Program.cs
index dfc4c77..e76f53a 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -127,7 +127,8 @@ using (IServiceScope scope = app.Services.CreateScope())
{
NotificationsContext context = scope.ServiceProvider.GetRequiredService();
context.Database.Migrate();
-
+
+ context.Notifications.RemoveRange(context.Notifications);
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
@@ -142,7 +143,6 @@ using (IServiceScope scope = app.Services.CreateScope())
context.Sync();
}
-TrangaSettings.Load();
Tranga.StartLogger();
Tranga.PeriodicWorkerStarterThread.Start(app.Services);
diff --git a/API/Schema/LibraryContext/LibraryConnectors/LibraryConnector.cs b/API/Schema/LibraryContext/LibraryConnectors/LibraryConnector.cs
index e58bba2..6b267cf 100644
--- a/API/Schema/LibraryContext/LibraryConnectors/LibraryConnector.cs
+++ b/API/Schema/LibraryContext/LibraryConnectors/LibraryConnector.cs
@@ -6,7 +6,7 @@ using Newtonsoft.Json;
namespace API.Schema.LibraryContext.LibraryConnectors;
-[PrimaryKey("LibraryConnectorId")]
+[PrimaryKey("Key")]
public abstract class LibraryConnector : Identifiable
{
[Required]
diff --git a/API/Schema/MangaContext/MetadataFetchers/MetadataEntry.cs b/API/Schema/MangaContext/MetadataFetchers/MetadataEntry.cs
index a239a08..ca0126d 100644
--- a/API/Schema/MangaContext/MetadataFetchers/MetadataEntry.cs
+++ b/API/Schema/MangaContext/MetadataFetchers/MetadataEntry.cs
@@ -3,7 +3,7 @@ using Newtonsoft.Json;
namespace API.Schema.MangaContext.MetadataFetchers;
-[PrimaryKey("Name", "Identifier")]
+[PrimaryKey("MetadataFetcherName", "Identifier")]
public class MetadataEntry
{
[JsonIgnore]
diff --git a/API/Tranga.cs b/API/Tranga.cs
index 1dea104..ab3fe72 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -1,11 +1,12 @@
using System.Diagnostics.CodeAnalysis;
+using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
+using API.Schema.NotificationsContext;
using API.Workers;
using API.Workers.MaintenanceWorkers;
using log4net;
using log4net.Config;
-using Microsoft.EntityFrameworkCore;
namespace API;
@@ -32,6 +33,7 @@ public static class Tranga
internal static readonly CheckForNewChaptersWorker CheckForNewChaptersWorker = new();
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
+ internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
internal static void StartLogger()
{
@@ -45,6 +47,7 @@ public static class Tranga
AddWorker(CheckForNewChaptersWorker);
AddWorker(CleanupMangaCoversWorker);
AddWorker(StartNewChapterDownloadsWorker);
+ AddWorker(RemoveOldNotificationsWorker);
}
internal static HashSet AllWorkers { get; private set; } = new ();
@@ -90,9 +93,22 @@ public static class Tranga
foreach (BaseWorker worker in StartWorkers)
{
- if (worker is BaseWorkerWithContext scopedWorker)
- scopedWorker.SetScope(serviceProvider.CreateScope());
- RunningWorkers.Add(worker, worker.DoWork());
+ if(RunningWorkers.ContainsKey(worker))
+ continue;
+ if (worker is BaseWorkerWithContext mangaContextWorker)
+ {
+ mangaContextWorker.SetScope(serviceProvider.CreateScope());
+ RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork());
+ }else if (worker is BaseWorkerWithContext notificationContextWorker)
+ {
+ notificationContextWorker.SetScope(serviceProvider.CreateScope());
+ RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork());
+ }else if (worker is BaseWorkerWithContext libraryContextWorker)
+ {
+ libraryContextWorker.SetScope(serviceProvider.CreateScope());
+ RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork());
+ }else
+ RunningWorkers.Add(worker, worker.DoWork());
}
Thread.Sleep(Settings.WorkCycleTimeoutMs);
}
diff --git a/API/TrangaSettings.cs b/API/TrangaSettings.cs
index 60b57d3..ceb1299 100644
--- a/API/TrangaSettings.cs
+++ b/API/TrangaSettings.cs
@@ -16,10 +16,10 @@ public struct TrangaSettings()
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
[JsonIgnore]
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
- public string UserAgent { get; private set; } = DefaultUserAgent;
- public int ImageCompression{ get; private set; } = 40;
- public bool BlackWhiteImages { get; private set; } = false;
- public string FlareSolverrUrl { get; private set; } = string.Empty;
+ public string UserAgent { get; set; } = DefaultUserAgent;
+ public int ImageCompression{ get; set; } = 40;
+ public bool BlackWhiteImages { get; set; } = false;
+ public string FlareSolverrUrl { get; set; } = string.Empty;
///
/// Placeholders:
/// %M Obj Name
@@ -34,8 +34,8 @@ public struct TrangaSettings()
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
///
- public string ChapterNamingScheme { get; private set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
- public int WorkCycleTimeoutMs { get; private set; } = 20000;
+ public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
+ public int WorkCycleTimeoutMs { get; set; } = 20000;
[JsonIgnore]
internal static readonly Dictionary DefaultRequestLimits = new ()
{
@@ -46,12 +46,14 @@ public struct TrangaSettings()
{RequestType.MangaCover, 60},
{RequestType.Default, 60}
};
- public Dictionary RequestLimits { get; private set; } = DefaultRequestLimits;
+ public Dictionary RequestLimits { get; set; } = DefaultRequestLimits;
- public string DownloadLanguage { get; private set; } = "en";
+ public string DownloadLanguage { get; set; } = "en";
public static TrangaSettings Load()
{
+ if (!File.Exists(settingsFilePath))
+ new TrangaSettings().Save();
return JsonConvert.DeserializeObject(File.ReadAllText(settingsFilePath));
}
diff --git a/API/Workers/BaseWorkerWithContext.cs b/API/Workers/BaseWorkerWithContext.cs
index eba54e9..9dc233b 100644
--- a/API/Workers/BaseWorkerWithContext.cs
+++ b/API/Workers/BaseWorkerWithContext.cs
@@ -6,7 +6,13 @@ namespace API.Workers;
public abstract class BaseWorkerWithContext(IEnumerable? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
{
protected T DbContext = null!;
- public void SetScope(IServiceScope scope) => DbContext = scope.ServiceProvider.GetRequiredService();
+ private IServiceScope? _scope;
+
+ public void SetScope(IServiceScope scope)
+ {
+ this._scope = scope;
+ this.DbContext = scope.ServiceProvider.GetRequiredService();
+ }
/// Scope has not been set.
public new Task DoWork()
diff --git a/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs b/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs
index d131f7c..a507fe4 100644
--- a/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs
+++ b/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs
@@ -2,7 +2,8 @@ using API.Schema.MangaContext;
namespace API.Workers.MaintenanceWorkers;
-public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null) : BaseWorkerWithContext(dependsOn), IPeriodic
+public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
+ : BaseWorkerWithContext(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
diff --git a/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs b/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs
new file mode 100644
index 0000000..357fb3a
--- /dev/null
+++ b/API/Workers/PeriodicWorkers/MaintenanceWorkers/RemoveOldNotificationsWorker.cs
@@ -0,0 +1,19 @@
+using API.Schema.NotificationsContext;
+
+namespace API.Workers.MaintenanceWorkers;
+
+public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable? dependsOn = null)
+ : BaseWorkerWithContext(dependsOn), IPeriodic
+{
+ public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
+ public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
+
+ protected override BaseWorker[] DoWorkInternal()
+ {
+ IQueryable