diff --git a/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
new file mode 100644
index 0000000..29f52b0
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
@@ -0,0 +1,688 @@
+//
+using System;
+using API.Schema.Contexts;
+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.pgsql
+{
+ [DbContext(typeof(PgsqlContext))]
+ [Migration("20250516121442_AltTitle-Owned")]
+ partial class AltTitleOwned
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.Author", b =>
+ {
+ b.Property("AuthorId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("AuthorName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("AuthorId");
+
+ b.ToTable("Authors");
+ });
+
+ modelBuilder.Entity("API.Schema.Chapter", b =>
+ {
+ b.Property("ChapterId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ 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()
+ .HasColumnType("character varying(64)");
+
+ b.Property("Title")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("VolumeNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("ChapterId");
+
+ b.HasIndex("ParentMangaId");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+ {
+ b.Property("JobId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("JobType")
+ .HasColumnType("smallint");
+
+ b.Property("LastExecution")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ParentJobId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("RecurrenceMs")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("state")
+ .HasColumnType("smallint");
+
+ b.HasKey("JobId");
+
+ b.HasIndex("ParentJobId");
+
+ b.ToTable("Jobs");
+
+ b.HasDiscriminator("JobType");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+ {
+ b.Property("LocalLibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("BasePath")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("LocalLibraryId");
+
+ b.ToTable("LocalLibraries");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.Property("MangaId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ 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("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("IgnoreChaptersBefore")
+ .HasColumnType("real");
+
+ b.Property("LibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ 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("WebsiteUrl")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Year")
+ .HasColumnType("bigint");
+
+ b.HasKey("MangaId");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.ToTable("Mangas");
+ });
+
+ modelBuilder.Entity("API.Schema.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.MangaTag", b =>
+ {
+ b.Property("Tag")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("Tag");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.Property("AuthorIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("AuthorIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("AuthorToManga");
+ });
+
+ modelBuilder.Entity("JobJob", b =>
+ {
+ b.Property("DependsOnJobsJobId")
+ .HasColumnType("character varying(64)");
+
+ b.Property("JobId")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("DependsOnJobsJobId", "JobId");
+
+ b.HasIndex("JobId");
+
+ b.ToTable("JobJob");
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.Property("MangaTagIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("MangaTagIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("MangaTagToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)1);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.HasDiscriminator().HasValue((byte)4);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("ChapterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("ChapterId");
+
+ b.HasDiscriminator().HasValue((byte)0);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("FromLocation")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ToLocation")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasDiscriminator().HasValue((byte)3);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("ToLibraryId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.HasIndex("ToLibraryId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("MoveMangaLibraryJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)7);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("RetrieveChaptersJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)5);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)6);
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("ComickIo");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("Global");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("MangaDex");
+ });
+
+ modelBuilder.Entity("API.Schema.Chapter", b =>
+ {
+ b.HasOne("API.Schema.Manga", "ParentManga")
+ .WithMany("Chapters")
+ .HasForeignKey("ParentMangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ParentManga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+ {
+ b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+ .WithMany()
+ .HasForeignKey("ParentJobId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("ParentJob");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.HasOne("API.Schema.LocalLibrary", "Library")
+ .WithMany()
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.OwnsMany("API.Schema.Link", "Links", b1 =>
+ {
+ b1.Property("LinkId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkProvider")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("MangaId")
+ .IsRequired()
+ .HasColumnType("character varying(64)");
+
+ b1.HasKey("LinkId");
+
+ b1.HasIndex("MangaId");
+
+ b1.ToTable("Link");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaId");
+ });
+
+ b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+ {
+ b1.Property("MangaId")
+ .HasColumnType("character varying(64)");
+
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
+
+ b1.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b1.Property("Title")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.HasKey("MangaId", "Id");
+
+ b1.ToTable("MangaAltTitle");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaId");
+ });
+
+ b.Navigation("AltTitles");
+
+ b.Navigation("Library");
+
+ b.Navigation("Links");
+
+ b.Navigation("MangaConnector");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.HasOne("API.Schema.Author", null)
+ .WithMany()
+ .HasForeignKey("AuthorIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("JobJob", b =>
+ {
+ b.HasOne("API.Schema.Jobs.Job", null)
+ .WithMany()
+ .HasForeignKey("DependsOnJobsJobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.Jobs.Job", null)
+ .WithMany()
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.HasOne("API.Schema.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaTag", null)
+ .WithMany()
+ .HasForeignKey("MangaTagIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+ {
+ b.HasOne("API.Schema.Chapter", "Chapter")
+ .WithMany()
+ .HasForeignKey("ChapterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Chapter");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+ .WithMany()
+ .HasForeignKey("ToLibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+
+ b.Navigation("ToLibrary");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.Navigation("Chapters");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
new file mode 100644
index 0000000..bde9cb6
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
@@ -0,0 +1,70 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+ ///
+ public partial class AltTitleOwned : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle");
+
+ migrationBuilder.DropIndex(
+ name: "IX_MangaAltTitle_MangaId",
+ table: "MangaAltTitle");
+
+ migrationBuilder.DropColumn(
+ name: "AltTitleId",
+ table: "MangaAltTitle");
+
+ migrationBuilder.AddColumn(
+ name: "Id",
+ table: "MangaAltTitle",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle",
+ columns: new[] { "MangaId", "Id" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle");
+
+ migrationBuilder.DropColumn(
+ name: "Id",
+ table: "MangaAltTitle");
+
+ migrationBuilder.AddColumn(
+ name: "AltTitleId",
+ table: "MangaAltTitle",
+ type: "character varying(64)",
+ maxLength: 64,
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle",
+ column: "AltTitleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaAltTitle_MangaId",
+ table: "MangaAltTitle",
+ column: "MangaId");
+ }
+ }
+}
diff --git a/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
new file mode 100644
index 0000000..afe97c3
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
@@ -0,0 +1,688 @@
+//
+using System;
+using API.Schema.Contexts;
+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.pgsql
+{
+ [DbContext(typeof(PgsqlContext))]
+ [Migration("20250516121725_Manga-Year-Nullable")]
+ partial class MangaYearNullable
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.Author", b =>
+ {
+ b.Property("AuthorId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("AuthorName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("AuthorId");
+
+ b.ToTable("Authors");
+ });
+
+ modelBuilder.Entity("API.Schema.Chapter", b =>
+ {
+ b.Property("ChapterId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ 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()
+ .HasColumnType("character varying(64)");
+
+ b.Property("Title")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("VolumeNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("ChapterId");
+
+ b.HasIndex("ParentMangaId");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+ {
+ b.Property("JobId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("JobType")
+ .HasColumnType("smallint");
+
+ b.Property("LastExecution")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ParentJobId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("RecurrenceMs")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("state")
+ .HasColumnType("smallint");
+
+ b.HasKey("JobId");
+
+ b.HasIndex("ParentJobId");
+
+ b.ToTable("Jobs");
+
+ b.HasDiscriminator("JobType");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+ {
+ b.Property("LocalLibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("BasePath")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("LocalLibraryId");
+
+ b.ToTable("LocalLibraries");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.Property("MangaId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ 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("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("IgnoreChaptersBefore")
+ .HasColumnType("real");
+
+ b.Property("LibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ 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("WebsiteUrl")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Year")
+ .HasColumnType("bigint");
+
+ b.HasKey("MangaId");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.ToTable("Mangas");
+ });
+
+ modelBuilder.Entity("API.Schema.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.MangaTag", b =>
+ {
+ b.Property("Tag")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("Tag");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.Property("AuthorIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("AuthorIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("AuthorToManga");
+ });
+
+ modelBuilder.Entity("JobJob", b =>
+ {
+ b.Property("DependsOnJobsJobId")
+ .HasColumnType("character varying(64)");
+
+ b.Property("JobId")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("DependsOnJobsJobId", "JobId");
+
+ b.HasIndex("JobId");
+
+ b.ToTable("JobJob");
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.Property("MangaTagIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("MangaTagIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("MangaTagToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)1);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.HasDiscriminator().HasValue((byte)4);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("ChapterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("ChapterId");
+
+ b.HasDiscriminator().HasValue((byte)0);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("FromLocation")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ToLocation")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasDiscriminator().HasValue((byte)3);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("ToLibraryId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.HasIndex("ToLibraryId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("MoveMangaLibraryJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)7);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("RetrieveChaptersJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)5);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)6);
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("ComickIo");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("Global");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("MangaDex");
+ });
+
+ modelBuilder.Entity("API.Schema.Chapter", b =>
+ {
+ b.HasOne("API.Schema.Manga", "ParentManga")
+ .WithMany("Chapters")
+ .HasForeignKey("ParentMangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ParentManga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+ {
+ b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+ .WithMany()
+ .HasForeignKey("ParentJobId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("ParentJob");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.HasOne("API.Schema.LocalLibrary", "Library")
+ .WithMany()
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.OwnsMany("API.Schema.Link", "Links", b1 =>
+ {
+ b1.Property("LinkId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkProvider")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("MangaId")
+ .IsRequired()
+ .HasColumnType("character varying(64)");
+
+ b1.HasKey("LinkId");
+
+ b1.HasIndex("MangaId");
+
+ b1.ToTable("Link");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaId");
+ });
+
+ b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+ {
+ b1.Property("MangaId")
+ .HasColumnType("character varying(64)");
+
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
+
+ b1.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b1.Property("Title")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.HasKey("MangaId", "Id");
+
+ b1.ToTable("MangaAltTitle");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaId");
+ });
+
+ b.Navigation("AltTitles");
+
+ b.Navigation("Library");
+
+ b.Navigation("Links");
+
+ b.Navigation("MangaConnector");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.HasOne("API.Schema.Author", null)
+ .WithMany()
+ .HasForeignKey("AuthorIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("JobJob", b =>
+ {
+ b.HasOne("API.Schema.Jobs.Job", null)
+ .WithMany()
+ .HasForeignKey("DependsOnJobsJobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.Jobs.Job", null)
+ .WithMany()
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.HasOne("API.Schema.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaTag", null)
+ .WithMany()
+ .HasForeignKey("MangaTagIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+ {
+ b.HasOne("API.Schema.Chapter", "Chapter")
+ .WithMany()
+ .HasForeignKey("ChapterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Chapter");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+ .WithMany()
+ .HasForeignKey("ToLibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+
+ b.Navigation("ToLibrary");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.Navigation("Chapters");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
new file mode 100644
index 0000000..9c0b7a8
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+ ///
+ public partial class MangaYearNullable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "Year",
+ table: "Mangas",
+ type: "bigint",
+ nullable: true,
+ oldClrType: typeof(long),
+ oldType: "bigint");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "Year",
+ table: "Mangas",
+ type: "bigint",
+ nullable: false,
+ defaultValue: 0L,
+ oldClrType: typeof(long),
+ oldType: "bigint",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
new file mode 100644
index 0000000..30b595c
--- /dev/null
+++ b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
@@ -0,0 +1,689 @@
+//
+using System;
+using API.Schema.Contexts;
+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.pgsql
+{
+ [DbContext(typeof(PgsqlContext))]
+ [Migration("20250516122242_AltTitle-Owned-WithId")]
+ partial class AltTitleOwnedWithId
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Schema.Author", b =>
+ {
+ b.Property("AuthorId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("AuthorName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("AuthorId");
+
+ b.ToTable("Authors");
+ });
+
+ modelBuilder.Entity("API.Schema.Chapter", b =>
+ {
+ b.Property("ChapterId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ 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()
+ .HasColumnType("character varying(64)");
+
+ b.Property("Title")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("VolumeNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("ChapterId");
+
+ b.HasIndex("ParentMangaId");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+ {
+ b.Property("JobId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("JobType")
+ .HasColumnType("smallint");
+
+ b.Property("LastExecution")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ParentJobId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("RecurrenceMs")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("state")
+ .HasColumnType("smallint");
+
+ b.HasKey("JobId");
+
+ b.HasIndex("ParentJobId");
+
+ b.ToTable("Jobs");
+
+ b.HasDiscriminator("JobType");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+ {
+ b.Property("LocalLibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("BasePath")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("LibraryName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("LocalLibraryId");
+
+ b.ToTable("LocalLibraries");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.Property("MangaId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ 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("IdOnConnectorSite")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("IgnoreChaptersBefore")
+ .HasColumnType("real");
+
+ b.Property("LibraryId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaConnectorName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ 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("WebsiteUrl")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Year")
+ .HasColumnType("bigint");
+
+ b.HasKey("MangaId");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("MangaConnectorName");
+
+ b.ToTable("Mangas");
+ });
+
+ modelBuilder.Entity("API.Schema.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.MangaTag", b =>
+ {
+ b.Property("Tag")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("Tag");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.Property("AuthorIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("AuthorIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("AuthorToManga");
+ });
+
+ modelBuilder.Entity("JobJob", b =>
+ {
+ b.Property("DependsOnJobsJobId")
+ .HasColumnType("character varying(64)");
+
+ b.Property("JobId")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("DependsOnJobsJobId", "JobId");
+
+ b.HasIndex("JobId");
+
+ b.ToTable("JobJob");
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.Property("MangaTagIds")
+ .HasColumnType("character varying(64)");
+
+ b.Property("MangaIds")
+ .HasColumnType("character varying(64)");
+
+ b.HasKey("MangaTagIds", "MangaIds");
+
+ b.HasIndex("MangaIds");
+
+ b.ToTable("MangaTagToManga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)1);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.HasDiscriminator().HasValue((byte)4);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("ChapterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("ChapterId");
+
+ b.HasDiscriminator().HasValue((byte)0);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("FromLocation")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ToLocation")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasDiscriminator().HasValue((byte)3);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("ToLibraryId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.HasIndex("ToLibraryId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("MoveMangaLibraryJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)7);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("RetrieveChaptersJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)5);
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+ {
+ b.HasBaseType("API.Schema.Jobs.Job");
+
+ b.Property("MangaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.HasIndex("MangaId");
+
+ b.ToTable("Jobs", t =>
+ {
+ t.Property("MangaId")
+ .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+ });
+
+ b.HasDiscriminator().HasValue((byte)6);
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("ComickIo");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("Global");
+ });
+
+ modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("MangaDex");
+ });
+
+ modelBuilder.Entity("API.Schema.Chapter", b =>
+ {
+ b.HasOne("API.Schema.Manga", "ParentManga")
+ .WithMany("Chapters")
+ .HasForeignKey("ParentMangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ParentManga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+ {
+ b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+ .WithMany()
+ .HasForeignKey("ParentJobId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("ParentJob");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.HasOne("API.Schema.LocalLibrary", "Library")
+ .WithMany()
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+ .WithMany()
+ .HasForeignKey("MangaConnectorName")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.OwnsMany("API.Schema.Link", "Links", b1 =>
+ {
+ b1.Property("LinkId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkProvider")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("LinkUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("MangaId")
+ .IsRequired()
+ .HasColumnType("character varying(64)");
+
+ b1.HasKey("LinkId");
+
+ b1.HasIndex("MangaId");
+
+ b1.ToTable("Link");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaId");
+ });
+
+ b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+ {
+ b1.Property("AltTitleId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b1.Property("Language")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b1.Property("MangaId")
+ .IsRequired()
+ .HasColumnType("character varying(64)");
+
+ b1.Property("Title")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.HasKey("AltTitleId");
+
+ b1.HasIndex("MangaId");
+
+ b1.ToTable("MangaAltTitle");
+
+ b1.WithOwner()
+ .HasForeignKey("MangaId");
+ });
+
+ b.Navigation("AltTitles");
+
+ b.Navigation("Library");
+
+ b.Navigation("Links");
+
+ b.Navigation("MangaConnector");
+ });
+
+ modelBuilder.Entity("AuthorToManga", b =>
+ {
+ b.HasOne("API.Schema.Author", null)
+ .WithMany()
+ .HasForeignKey("AuthorIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("JobJob", b =>
+ {
+ b.HasOne("API.Schema.Jobs.Job", null)
+ .WithMany()
+ .HasForeignKey("DependsOnJobsJobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.Jobs.Job", null)
+ .WithMany()
+ .HasForeignKey("JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MangaTagToManga", b =>
+ {
+ b.HasOne("API.Schema.Manga", null)
+ .WithMany()
+ .HasForeignKey("MangaIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.MangaTag", null)
+ .WithMany()
+ .HasForeignKey("MangaTagIds")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+ {
+ b.HasOne("API.Schema.Chapter", "Chapter")
+ .WithMany()
+ .HasForeignKey("ChapterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Chapter");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+ .WithMany()
+ .HasForeignKey("ToLibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+
+ b.Navigation("ToLibrary");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+ {
+ b.HasOne("API.Schema.Manga", "Manga")
+ .WithMany()
+ .HasForeignKey("MangaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manga");
+ });
+
+ modelBuilder.Entity("API.Schema.Manga", b =>
+ {
+ b.Navigation("Chapters");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
new file mode 100644
index 0000000..3814d95
--- /dev/null
+++ b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
@@ -0,0 +1,70 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+ ///
+ public partial class AltTitleOwnedWithId : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle");
+
+ migrationBuilder.DropColumn(
+ name: "Id",
+ table: "MangaAltTitle");
+
+ migrationBuilder.AddColumn(
+ name: "AltTitleId",
+ table: "MangaAltTitle",
+ type: "character varying(64)",
+ maxLength: 64,
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle",
+ column: "AltTitleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaAltTitle_MangaId",
+ table: "MangaAltTitle",
+ column: "MangaId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle");
+
+ migrationBuilder.DropIndex(
+ name: "IX_MangaAltTitle_MangaId",
+ table: "MangaAltTitle");
+
+ migrationBuilder.DropColumn(
+ name: "AltTitleId",
+ table: "MangaAltTitle");
+
+ migrationBuilder.AddColumn(
+ name: "Id",
+ table: "MangaAltTitle",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_MangaAltTitle",
+ table: "MangaAltTitle",
+ columns: new[] { "MangaId", "Id" });
+ }
+ }
+}
diff --git a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
index d7dd6c6..623bec6 100644
--- a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
+++ b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
@@ -195,7 +195,7 @@ namespace API.Migrations.pgsql
.HasMaxLength(512)
.HasColumnType("character varying(512)");
- b.Property("Year")
+ b.Property("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
@@ -433,6 +433,13 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)6);
});
+ modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+ {
+ b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+ b.HasDiscriminator().HasValue("ComickIo");
+ });
+
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
diff --git a/API/Program.cs b/API/Program.cs
index e9f59a5..1953c47 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -113,6 +113,7 @@ using (IServiceScope scope = app.Services.CreateScope())
MangaConnector[] connectors =
[
new MangaDex(),
+ new ComickIo(),
new Global(scope.ServiceProvider.GetService()!)
];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index fc1f1f9..2b138e2 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -1,5 +1,4 @@
using API.Schema.Jobs;
-using API.Schema.LibraryConnectors;
using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore;
@@ -112,7 +111,8 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op
modelBuilder.Entity()
.HasDiscriminator(c => c.Name)
.HasValue("Global")
- .HasValue("MangaDex");
+ .HasValue("MangaDex")
+ .HasValue("ComickIo");
//MangaConnector is responsible for many Manga
modelBuilder.Entity()
.HasMany()
diff --git a/API/Schema/MangaConnectors/ComickIo.cs b/API/Schema/MangaConnectors/ComickIo.cs
new file mode 100644
index 0000000..cb8a5c2
--- /dev/null
+++ b/API/Schema/MangaConnectors/ComickIo.cs
@@ -0,0 +1,246 @@
+using System.Text.RegularExpressions;
+using API.MangaDownloadClients;
+using Newtonsoft.Json.Linq;
+
+namespace API.Schema.MangaConnectors;
+
+public class ComickIo : MangaConnector
+{
+ //https://api.comick.io/docs/
+ //https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
+
+ public ComickIo() : base("ComickIo",
+ ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
+ ["comick.io"],
+ "https://comick.io/static/icons/unicorn-64.png")
+ {
+ this.downloadClient = new HttpDownloadClient();
+ }
+
+ public override Manga[] SearchManga(string mangaSearchName)
+ {
+ Log.Info($"Searching Manga: {mangaSearchName}");
+
+ List slugs = new();
+ int page = 1;
+ while(page < 50)
+ {
+ string requestUrl = $"https://api.comick.fun/v1.0/search/?type=comic&t=false&limit=100&showall=true&" +
+ $"page={page}&q={mangaSearchName}";
+
+ RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+ if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+ {
+ Log.Error("Request failed");
+ return [];
+ }
+
+ using StreamReader sr = new (result.result);
+ JArray data = JArray.Parse(sr.ReadToEnd());
+
+ if (data.Count < 1)
+ break;
+
+ slugs.AddRange(data.Select(token => token.Value("slug")!));
+ page++;
+ }
+ Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
+
+ List mangas = slugs.Select(GetMangaFromId).ToList()!;
+
+ Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
+ return mangas.ToArray();
+ }
+
+ private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*");
+ public override Manga? GetMangaFromUrl(string url)
+ {
+ Match m = _getSlugFromTitleRex.Match(url);
+ return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null;
+ }
+
+ public override Manga? GetMangaFromId(string mangaIdOnSite)
+ {
+ string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
+
+ RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+ if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+ {
+ Log.Error("Request failed");
+ return null;
+ }
+ using StreamReader sr = new (result.result);
+ JToken data = JToken.Parse(sr.ReadToEnd());
+
+ return ParseMangaFromJToken(data);
+ }
+
+ public override Chapter[] GetChapters(Manga manga, string? language = null)
+ {
+ Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
+ List chapterHids = new();
+ int page = 1;
+ while(page < 50)
+ {
+ string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}";
+
+ RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+ if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+ {
+ Log.Error("Request failed");
+ return [];
+ }
+
+ using StreamReader sr = new (result.result);
+ JArray data = JArray.Parse(sr.ReadToEnd());
+
+ if (data.Count < 1)
+ break;
+
+ chapterHids.AddRange(data.Select(token => token.Value("hid")!));
+
+ page++;
+ }
+ Log.Debug($"Getting chapters for {manga.Name} yielded {chapterHids.Count} hids. Requesting chapters now...");
+
+ List chapters = chapterHids.Select(hid => ChapterFromHid(manga, hid)).ToList();
+
+ return chapters.ToArray();
+ }
+
+ private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
+ internal override string[] GetChapterImageUrls(Chapter chapter)
+ {
+ Match m = _hidFromUrl.Match(chapter.Url);
+ if (!m.Groups[1].Success)
+ return [];
+
+ string hid = m.Groups[1].Value;
+
+ string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
+ RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+ if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+ {
+ Log.Error("Request failed");
+ return [];
+ }
+
+ using StreamReader sr = new (result.result);
+ JArray data = JArray.Parse(sr.ReadToEnd());
+
+ return data.Select(token =>
+ {
+ string url = $"https://meo.comick.pictures/{token.Value("b2key")}";
+ return url;
+ }).ToArray();
+ }
+
+ private Manga ParseMangaFromJToken(JToken json)
+ {
+ string? hid = json["comic"]?.Value("hid");
+ string? slug = json["comic"]?.Value("slug");
+ string? name = json["comic"]?.Value("title");
+ string? description = json["comic"]?.Value("desc");
+ string? originalLanguage = json["comic"]?.Value("country");
+ string url = $"https://comick.io/comic/{slug}";
+ string? coverName = json["comic"]?["md_covers"]?.First?.Value("b2key");
+ string coverUrl = $"https://meo.comick.pictures/{coverName}";
+ int? releaseStatusStr = json["comic"]?.Value("status");
+ MangaReleaseStatus status = releaseStatusStr switch
+ {
+ 1 => MangaReleaseStatus.Continuing,
+ 2 => MangaReleaseStatus.Completed,
+ 3 => MangaReleaseStatus.Cancelled,
+ 4 => MangaReleaseStatus.OnHiatus,
+ _ => MangaReleaseStatus.Unreleased
+ };
+ uint? year = json["comic"]?.Value("year");
+ JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray;
+ //Cant let language be null, so fill with whatever.
+ byte whatever = 0;
+ List altTitles = altTitlesArray?
+ .Select(token => new MangaAltTitle(token.Value("lang")??whatever++.ToString(), token.Value("title")!))
+ .ToList()!;
+
+ JArray? authorsArray = json["authors"] as JArray;
+ JArray? artistsArray = json["artists"] as JArray;
+ List authors = authorsArray?.Concat(artistsArray!)
+ .Select(token => new Author(token.Value("name")!))
+ .DistinctBy(a => a.AuthorId)
+ .ToList()!;
+
+ JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
+ List tags = genreArray?
+ .Select(token => new MangaTag(token["md_genres"]?.Value("name")!))
+ .ToList()!;
+
+ JArray? linksArray = json["comic"]?["links"] as JArray;
+ List links = linksArray?
+ .ToObject>()?
+ .Select(kv =>
+ {
+ string fullUrl = kv.Key switch
+ {
+ "al" => $"https://anilist.co/manga/{kv.Value}",
+ "ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
+ "bw" => $"https://bookwalker.jp/{kv.Value}",
+ "mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
+ "nu" => $"https://www.novelupdates.com/series/{kv.Value}",
+ "mal" => $"https://myanimelist.net/manga/{kv.Value}",
+ _ => kv.Value
+ };
+ string key = kv.Key switch
+ {
+ "al" => "AniList",
+ "ap" => "Anime Planet",
+ "bw" => "BookWalker",
+ "mu" => "Manga Updates",
+ "nu" => "Novel Updates",
+ "kt" => "Kitsu.io",
+ "amz" => "Amazon",
+ "ebj" => "eBookJapan",
+ "mal" => "MyAnimeList",
+ "cdj" => "CDJapan",
+ _ => kv.Key
+ };
+ return new Link(key, fullUrl);
+ }).ToList()!;
+
+ if(hid is null)
+ throw new Exception("hid is null");
+ if(slug is null)
+ throw new Exception("slug is null");
+ if(name is null)
+ throw new Exception("name is null");
+
+ return new Manga(hid, name, description??"", url, coverUrl, status, this,
+ authors, tags, links, altTitles,
+ year: year, originalLanguage: originalLanguage);
+ }
+
+ private Chapter ChapterFromHid(Manga parentManga, string hid)
+ {
+ string requestUrl = $"https://api.comick.fun/chapter/{hid}";
+ RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+ if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+ {
+ Log.Error("Request failed");
+ throw new Exception("Request failed");
+ }
+
+ using StreamReader sr = new (result.result);
+ JToken data = JToken.Parse(sr.ReadToEnd());
+
+ string? canonical = data.Value("canonical");
+ string? chapterNum = data["chapter"]?.Value("chap");
+ string? volumeNumStr = data["chapter"]?.Value("vol");
+ int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
+ string? title = data["chapter"]?.Value("title");
+
+ if(chapterNum is null)
+ throw new Exception("chapterNum is null");
+
+ string url = $"https://comick.io{canonical}";
+ return new Chapter(parentManga, url, chapterNum, volumeNum, title);
+ }
+}
\ No newline at end of file