From ebe7e145aaec482dc47317a44a3a0274c8f47daa Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 25 Jan 2025 11:57:54 +0100 Subject: [PATCH 01/10] [postgres-Server-V2] feat: Convert chapterNumeber to string --- .../20250111180034_ChapterNumber.Designer.cs | 693 ++++++++++++++++++ .../20250111180034_ChapterNumber.cs | 54 ++ API/Migrations/PgsqlContextModelSnapshot.cs | 10 +- API/Schema/Author.cs | 2 +- API/Schema/Chapter.cs | 107 +-- API/Schema/ChapterNumber.cs | 305 -------- API/Schema/Jobs/DownloadMangaCoverJob.cs | 2 +- API/Schema/Jobs/DownloadNewChaptersJob.cs | 2 +- API/Schema/Jobs/DownloadSingleChapterJob.cs | 2 +- API/Schema/Jobs/MoveFileOrFolderJob.cs | 2 +- API/Schema/Jobs/UpdateMetadataJob.cs | 5 +- API/Schema/LibraryConnectors/Kavita.cs | 2 +- API/Schema/LibraryConnectors/Komga.cs | 2 +- API/Schema/Link.cs | 2 +- API/Schema/Manga.cs | 3 +- API/Schema/MangaAltTitle.cs | 2 +- API/Schema/MangaConnectors/AsuraToon.cs | 4 +- API/Schema/MangaConnectors/Bato.cs | 4 +- API/Schema/MangaConnectors/MangaDex.cs | 4 +- API/Schema/MangaConnectors/MangaHere.cs | 4 +- API/Schema/MangaConnectors/MangaKatana.cs | 5 +- API/Schema/MangaConnectors/MangaLife.cs | 5 +- API/Schema/MangaConnectors/Manganato.cs | 4 +- API/Schema/MangaConnectors/Mangaworld.cs | 10 +- API/Schema/MangaConnectors/ManhuaPlus.cs | 4 +- API/Schema/MangaConnectors/WeebCentral.cs | 15 +- API/Schema/Notification.cs | 2 +- API/Schema/NotificationConnectors/Gotify.cs | 2 +- API/Schema/NotificationConnectors/Lunasea.cs | 2 +- API/Schema/NotificationConnectors/Ntfy.cs | 2 +- API/TokenGen.cs | 49 +- 31 files changed, 881 insertions(+), 430 deletions(-) create mode 100644 API/Migrations/20250111180034_ChapterNumber.Designer.cs create mode 100644 API/Migrations/20250111180034_ChapterNumber.cs delete mode 100644 API/Schema/ChapterNumber.cs diff --git a/API/Migrations/20250111180034_ChapterNumber.Designer.cs b/API/Migrations/20250111180034_ChapterNumber.Designer.cs new file mode 100644 index 0000000..e330827 --- /dev/null +++ b/API/Migrations/20250111180034_ChapterNumber.Designer.cs @@ -0,0 +1,693 @@ +// +using System; +using API.Schema; +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 +{ + [DbContext(typeof(PgsqlContext))] + [Migration("20250111180034_ChapterNumber")] + partial class ChapterNumber + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .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() + .HasColumnType("text"); + + b.HasKey("AuthorId"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("ChapterId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ArchiveFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("ParentMangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + 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.PrimitiveCollection("DependsOnJobsIds") + .HasMaxLength(64) + .HasColumnType("text[]"); + + b.Property("JobId1") + .HasColumnType("character varying(64)"); + + 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("integer"); + + b.HasKey("JobId"); + + b.HasIndex("JobId1"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => + { + b.Property("LibraryConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("LibraryType") + .HasColumnType("smallint"); + + b.HasKey("LibraryConnectorId"); + + b.ToTable("LibraryConnectors"); + + b.HasDiscriminator("LibraryType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.Property("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LinkProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("LinkId"); + + b.HasIndex("MangaId"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("MangaId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConnectorId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasColumnType("text"); + + b.Property("CoverUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IgnoreChapterBefore") + .HasColumnType("real"); + + b.Property("MangaConnectorId") + .IsRequired() + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalLanguage") + .HasColumnType("text"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("MangaId"); + + b.HasIndex("MangaConnectorId"); + + b.ToTable("Manga"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.Property("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AltTitleId"); + + b.HasIndex("MangaId"); + + b.ToTable("AltTitles"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasColumnType("text[]"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnectors"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaTag", b => + { + b.Property("Tag") + .HasColumnType("text"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("API.Schema.Notification", b => + { + b.Property("NotificationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Urgency") + .HasColumnType("smallint"); + + b.HasKey("NotificationId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => + { + b.Property("NotificationConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NotificationConnectorType") + .HasColumnType("smallint"); + + b.HasKey("NotificationConnectorId"); + + b.ToTable("NotificationConnectors"); + + b.HasDiscriminator("NotificationConnectorType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.Property("AuthorsAuthorId") + .HasColumnType("character varying(64)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorsAuthorId", "MangaId"); + + b.HasIndex("MangaId"); + + b.ToTable("AuthorManga"); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("TagsTag") + .HasColumnType("text"); + + b.HasKey("MangaId", "TagsTag"); + + b.HasIndex("TagsTag"); + + b.ToTable("MangaMangaTag"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + 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() + .HasColumnType("text"); + + b.Property("ToLocation") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue((byte)3); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", 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("UpdateMetadataJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)2); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("AsuraToon"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Bato"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaDex"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaHere"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaKatana"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaLife", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Manga4Life"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Manganato"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Mangasee", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangasee"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangaworld"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("ManhuaPlus"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Weebcentral"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.Gotify", b => + { + b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector"); + + b.Property("AppToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.Lunasea", b => + { + b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector"); + + b.Property("Id") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.Ntfy", b => + { + b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text"); + + b.ToTable("NotificationConnectors", t => + { + t.Property("Endpoint") + .HasColumnName("Ntfy_Endpoint"); + }); + + b.HasDiscriminator().HasValue((byte)2); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.HasOne("API.Schema.Manga", "ParentManga") + .WithMany() + .HasForeignKey("ParentMangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParentManga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.HasOne("API.Schema.Jobs.Job", null) + .WithMany("DependsOnJobs") + .HasForeignKey("JobId1"); + + b.HasOne("API.Schema.Jobs.Job", "ParentJob") + .WithMany() + .HasForeignKey("ParentJobId"); + + b.Navigation("ParentJob"); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("Links") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MangaConnector"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("AltTitles") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.HasOne("API.Schema.Author", null) + .WithMany() + .HasForeignKey("AuthorsAuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaTag", null) + .WithMany() + .HasForeignKey("TagsTag") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", 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.UpdateMetadataJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Navigation("DependsOnJobs"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Navigation("AltTitles"); + + b.Navigation("Links"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20250111180034_ChapterNumber.cs b/API/Migrations/20250111180034_ChapterNumber.cs new file mode 100644 index 0000000..ef0ba03 --- /dev/null +++ b/API/Migrations/20250111180034_ChapterNumber.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class ChapterNumber : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "VolumeNumber", + table: "Chapters", + type: "integer", + nullable: true, + oldClrType: typeof(float), + oldType: "real", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ChapterNumber", + table: "Chapters", + type: "character varying(10)", + maxLength: 10, + nullable: false, + oldClrType: typeof(float), + oldType: "real"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "VolumeNumber", + table: "Chapters", + type: "real", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ChapterNumber", + table: "Chapters", + type: "real", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10); + } + } +} diff --git a/API/Migrations/PgsqlContextModelSnapshot.cs b/API/Migrations/PgsqlContextModelSnapshot.cs index 41a4ae8..a0499ab 100644 --- a/API/Migrations/PgsqlContextModelSnapshot.cs +++ b/API/Migrations/PgsqlContextModelSnapshot.cs @@ -47,8 +47,10 @@ namespace API.Migrations .IsRequired() .HasColumnType("text"); - b.Property("ChapterNumber") - .HasColumnType("real"); + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); b.Property("Downloaded") .HasColumnType("boolean"); @@ -64,8 +66,8 @@ namespace API.Migrations .IsRequired() .HasColumnType("text"); - b.Property("VolumeNumber") - .HasColumnType("real"); + b.Property("VolumeNumber") + .HasColumnType("integer"); b.HasKey("ChapterId"); diff --git a/API/Schema/Author.cs b/API/Schema/Author.cs index 7b64e66..583f33e 100644 --- a/API/Schema/Author.cs +++ b/API/Schema/Author.cs @@ -7,6 +7,6 @@ namespace API.Schema; public class Author(string authorName) { [MaxLength(64)] - public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), 64); + public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), authorName); public string AuthorName { get; init; } = authorName; } \ No newline at end of file diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs index f236e89..5c9ce27 100644 --- a/API/Schema/Chapter.cs +++ b/API/Schema/Chapter.cs @@ -8,71 +8,84 @@ namespace API.Schema; [PrimaryKey("ChapterId")] public class Chapter : IComparable { - [MaxLength(64)] - public string ChapterId { get; init; } = TokenGen.CreateToken(typeof(Chapter), 64); + public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null) + : this(parentManga.MangaId, url, chapterNumber, volumeNumber, title) + { + ParentManga = parentManga; + ArchiveFileName = BuildArchiveFileName(); + } + + public Chapter(string parentMangaId, string url, string chapterNumber, + int? volumeNumber = null, string? title = null) + { + ChapterId = TokenGen.CreateToken(typeof(Chapter), string.Join(parentMangaId, volumeNumber ?? 0, chapterNumber)); + ParentMangaId = parentMangaId; + Url = url; + ChapterNumber = chapterNumber; + VolumeNumber = volumeNumber; + Title = title; + } + + [MaxLength(64)] public string ChapterId { get; init; } + public int? VolumeNumber { get; private set; } - public ChapterNumber ChapterNumber { get; private set; } + + [MaxLength(10)] public string ChapterNumber { get; private set; } + public string Url { get; internal set; } public string? Title { get; private set; } public string ArchiveFileName { get; private set; } public bool Downloaded { get; internal set; } = false; - + public string ParentMangaId { get; internal set; } public Manga? ParentManga { get; init; } - public Chapter(Manga parentManga, string url, ChapterNumber chapterNumber, int? volumeNumber = null, string? title = null) - : this(parentManga.MangaId, url, chapterNumber, volumeNumber, title) + public int CompareTo(Chapter? other) { - this.ParentManga = parentManga; - } - - public Chapter(string parentMangaId, string url, ChapterNumber chapterNumber, - int? volumeNumber = null, string? title = null) - { - this.ParentMangaId = parentMangaId; - this.Url = url; - this.ChapterNumber = chapterNumber; - this.VolumeNumber = volumeNumber; - this.Title = title; - this.ArchiveFileName = BuildArchiveFileName(); + if (other is not { } otherChapter) + throw new ArgumentException($"{other} can not be compared to {this}"); + return VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch + { + < 0 => -1, + > 0 => 1, + _ => CompareChapterNumbers(ChapterNumber, otherChapter.ChapterNumber) + }; } - public MoveFileOrFolderJob? UpdateChapterNumber(ChapterNumber chapterNumber) + public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber) { - this.ChapterNumber = chapterNumber; + ChapterNumber = chapterNumber; return UpdateArchiveFileName(); } public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber) { - this.VolumeNumber = volumeNumber; + VolumeNumber = volumeNumber; return UpdateArchiveFileName(); } public MoveFileOrFolderJob? UpdateTitle(string? title) { - this.Title = title; + Title = title; return UpdateArchiveFileName(); } private string BuildArchiveFileName() { - return $"{this.ParentManga.Name} - Vol.{this.VolumeNumber ?? 0} Ch.{this.ChapterNumber}{(this.Title is null ? "" : $" - {this.Title}")}.cbz"; + return + $"{ParentManga.Name} - Vol.{VolumeNumber ?? 0} Ch.{ChapterNumber}{(Title is null ? "" : $" - {Title}")}.cbz"; } private MoveFileOrFolderJob? UpdateArchiveFileName() { string oldPath = GetArchiveFilePath(); - this.ArchiveFileName = BuildArchiveFileName(); - if (Downloaded) - { - return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath()); - } + ArchiveFileName = BuildArchiveFileName(); + if (Downloaded) return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath()); return null; } - + /// - /// Creates full file path of chapter-archive + /// Creates full file path of chapter-archive /// /// Filepath internal string GetArchiveFilePath() @@ -86,27 +99,35 @@ public class Chapter : IComparable return File.Exists(path); } - public int CompareTo(Chapter? other) + private static int CompareChapterNumbers(string ch1, string ch2) { - if(other is not { } otherChapter) - throw new ArgumentException($"{other} can not be compared to {this}"); - return this.VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch + int[] ch1Arr = ch1.Split('.').Select(c => int.Parse(c)).ToArray(); + int[] ch2Arr = ch2.Split('.').Select(c => int.Parse(c)).ToArray(); + + int i = 0, j = 0; + + while (i < ch1Arr.Length && j < ch2Arr.Length) { - <0 => -1, - >0 => 1, - _ => this.ChapterNumber.CompareTo(otherChapter.ChapterNumber) - }; + if (ch1Arr[i] < ch2Arr[j]) + return -1; + if (ch1Arr[i] > ch2Arr[j]) + return 1; + i++; + j++; + } + + return 0; } - + internal string GetComicInfoXmlString() { - XElement comicInfo = new XElement("ComicInfo", + XElement comicInfo = new("ComicInfo", new XElement("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))), new XElement("LanguageISO", ParentManga.OriginalLanguage), - new XElement("Title", this.Title), + new XElement("Title", Title), new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))), - new XElement("Volume", this.VolumeNumber), - new XElement("Number", this.ChapterNumber) + new XElement("Volume", VolumeNumber), + new XElement("Number", ChapterNumber) ); return comicInfo.ToString(); } diff --git a/API/Schema/ChapterNumber.cs b/API/Schema/ChapterNumber.cs deleted file mode 100644 index 240ff7c..0000000 --- a/API/Schema/ChapterNumber.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Numerics; -using System.Text.RegularExpressions; - -namespace API.Schema; - -public readonly struct ChapterNumber : INumber -{ - private readonly uint[] _numbers; - private readonly bool _naN; - - private ChapterNumber(uint[] numbers, bool naN = false) - { - this._numbers = numbers; - this._naN = naN; - } - - public ChapterNumber(string number) - { - if (!CanParse(number)) - { - this._numbers = []; - this._naN = true; - } - this._numbers = number.Split('.').Select(uint.Parse).ToArray(); - } - - public ChapterNumber(float number) : this(number.ToString("F")) {} - - public ChapterNumber(double number) : this((float)number) {} - - public ChapterNumber(uint number) - { - this._numbers = [number]; - this._naN = false; - } - - public ChapterNumber(int number) - { - if (int.IsNegative(number)) - { - this._numbers = []; - this._naN = true; - } - this._numbers = [(uint)number]; - this._naN = false; - } - - public int CompareTo(ChapterNumber other) - { - byte index = 0; - do - { - if (this._numbers[index] < other._numbers[index]) - return -1; - else if (this._numbers[index] > other._numbers[index]) - return 1; - }while(index < this._numbers.Length && index < other._numbers.Length); - - if (index >= this._numbers.Length && index >= other._numbers.Length) - return 0; - else if (index >= this._numbers.Length) - return -1; - else if (index >= other._numbers.Length) - return 1; - throw new UnreachableException(); - } - - private static readonly Regex Pattern = new(@"[0-9]+(?:\.[0-9]+)*"); - public static bool CanParse(string? number) => number is not null && Pattern.Match(number).Length == number.Length && number.Length > 0; - - public bool Equals(ChapterNumber other) => CompareTo(other) == 0; - - public string ToString(string? format, IFormatProvider? formatProvider) - { - return string.Join('.', _numbers); - } - - public override bool Equals(object? obj) - { - return obj is ChapterNumber other && Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Combine(_numbers, _naN); - } - - public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) - { - throw new NotImplementedException(); - } - - public int CompareTo(object? obj) - { - if(obj is ChapterNumber other) - return CompareTo(other); - throw new ArgumentException(); - } - - public static ChapterNumber Parse(string s, IFormatProvider? provider) - { - if(!CanParse(s)) - throw new FormatException($"Invalid ChapterNumber-String: {s}"); - return new ChapterNumber(s); - } - - public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out ChapterNumber result) - { - result = new ChapterNumber([], true);; - if (!CanParse(s)) - return false; - if (s is null) - return false; - result = new ChapterNumber(s); - return true; - } - - public static ChapterNumber Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s.ToString(), provider); - - public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out ChapterNumber result) => TryParse(s.ToString(), provider, out result); - - public static ChapterNumber operator +(ChapterNumber left, ChapterNumber right) - { - if (IsNaN(left) || IsNaN(right)) - return new ChapterNumber([], true); - int size = left._numbers.Length > right._numbers.Length ? left._numbers.Length : right._numbers.Length; - uint[] numbers = new uint[size]; - for (int i = 0; i < size; i++) - { - if(left._numbers.Length < i) - numbers[i] = right._numbers[i]; - else if(right._numbers.Length < i) - numbers[i] = left._numbers[i]; - else - numbers[i] = left._numbers[i] + right._numbers[i]; - } - return new ChapterNumber(numbers); - } - - private static bool BothNotNaN(ChapterNumber left, ChapterNumber right) => !IsNaN(left) && !IsNaN(right); - - public static ChapterNumber AdditiveIdentity => Zero; - - public static bool operator ==(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.Equals(right); - - public static bool operator !=(ChapterNumber left, ChapterNumber right) => !(left == right); - - public static bool operator >(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) > 0; - - public static bool operator >=(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) >= 0; - - public static bool operator <(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) < 0; - - public static bool operator <=(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) <= 0; - - public static ChapterNumber operator %(ChapterNumber left, ChapterNumber right) => throw new ArithmeticException(); - - public static ChapterNumber operator +(ChapterNumber value) => throw new InvalidOperationException(); - - public static ChapterNumber operator --(ChapterNumber value) - { - if (IsNaN(value)) - return value; - uint[] numbers = value._numbers; - numbers[0]--; - return new ChapterNumber(numbers); - } - - public static ChapterNumber operator /(ChapterNumber left, ChapterNumber right) => throw new InvalidOperationException(); - - public static ChapterNumber operator ++(ChapterNumber value) - { - if (IsNaN(value)) - return value; - uint[] numbers = value._numbers; - numbers[0]++; - return new ChapterNumber(numbers); - } - - public static ChapterNumber MultiplicativeIdentity => One; - public static ChapterNumber operator *(ChapterNumber left, ChapterNumber right) => throw new InvalidOperationException(); - - public static ChapterNumber operator -(ChapterNumber left, ChapterNumber right) => throw new InvalidOperationException(); - - public static ChapterNumber operator -(ChapterNumber value) => throw new InvalidOperationException(); - - public static ChapterNumber Abs(ChapterNumber value) => value; - - public static bool IsCanonical(ChapterNumber value) => true; - - public static bool IsComplexNumber(ChapterNumber value) => false; - - public static bool IsEvenInteger(ChapterNumber value) => IsInteger(value) && uint.IsEvenInteger(value._numbers[0]); - - public static bool IsFinite(ChapterNumber value) => true; - - public static bool IsImaginaryNumber(ChapterNumber value) => false; - - public static bool IsInfinity(ChapterNumber value) => false; - - public static bool IsInteger(ChapterNumber value) => !IsNaN(value) && value._numbers.Length == 1; - - public static bool IsNaN(ChapterNumber value) => value._naN; - - public static bool IsNegative(ChapterNumber value) => false; - - public static bool IsNegativeInfinity(ChapterNumber value) => false; - - public static bool IsNormal(ChapterNumber value) => true; - - public static bool IsOddInteger(ChapterNumber value) => false; - - public static bool IsPositive(ChapterNumber value) => true; - - public static bool IsPositiveInfinity(ChapterNumber value) => false; - - public static bool IsRealNumber(ChapterNumber value) => false; - - public static bool IsSubnormal(ChapterNumber value) => false; - - public static bool IsZero(ChapterNumber value) => value._numbers.All(n => n == 0); - - public static ChapterNumber MaxMagnitude(ChapterNumber x, ChapterNumber y) - { - if(IsNaN(x)) - return new ChapterNumber([], true); - if (IsNaN(y)) - return new ChapterNumber([], true); - return x >= y ? x : y; - } - - public static ChapterNumber MaxMagnitudeNumber(ChapterNumber x, ChapterNumber y) - { - if (IsNaN(x)) - return y; - if (IsNaN(y)) - return x; - return x >= y ? x : y; - } - - public static ChapterNumber MinMagnitude(ChapterNumber x, ChapterNumber y) - { - if(IsNaN(x)) - return new ChapterNumber([], true); - if (IsNaN(y)) - return new ChapterNumber([], true); - return x <= y ? x : y; - } - - public static ChapterNumber MinMagnitudeNumber(ChapterNumber x, ChapterNumber y) - { - if (IsNaN(x)) - return y; - if (IsNaN(y)) - return x; - return x <= y ? x : y; - } - - public static ChapterNumber Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider) => throw new NotImplementedException(); - - public static ChapterNumber Parse(string s, NumberStyles style, IFormatProvider? provider) => throw new NotImplementedException(); - - public static bool TryConvertFromChecked(TOther value, out ChapterNumber result) where TOther : INumberBase - { - throw new NotImplementedException(); - } - - public static bool TryConvertFromSaturating(TOther value, out ChapterNumber result) where TOther : INumberBase - { - throw new NotImplementedException(); - } - - public static bool TryConvertFromTruncating(TOther value, out ChapterNumber result) where TOther : INumberBase - { - throw new NotImplementedException(); - } - - public static bool TryConvertToChecked(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase - { - throw new NotImplementedException(); - } - - public static bool TryConvertToSaturating(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase - { - throw new NotImplementedException(); - } - - public static bool TryConvertToTruncating(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase - { - throw new NotImplementedException(); - } - - public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out ChapterNumber result) - => TryParse(s.ToString(), style, provider, out result); - - public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out ChapterNumber result) - => TryParse(s, provider, out result); - - public static ChapterNumber One => new(1); - public static int Radix => 10; - public static ChapterNumber Zero => new(0); -} \ No newline at end of file diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs index 4de3a6b..e6ef6d0 100644 --- a/API/Schema/Jobs/DownloadMangaCoverJob.cs +++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs @@ -12,7 +12,7 @@ using static System.IO.UnixFileMode; namespace API.Schema.Jobs; public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob), 64), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob), ""), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string ChapterId { get; init; } = chapterId; diff --git a/API/Schema/Jobs/DownloadNewChaptersJob.cs b/API/Schema/Jobs/DownloadNewChaptersJob.cs index 241a051..b090d0b 100644 --- a/API/Schema/Jobs/DownloadNewChaptersJob.cs +++ b/API/Schema/Jobs/DownloadNewChaptersJob.cs @@ -4,7 +4,7 @@ using API.Schema.MangaConnectors; namespace API.Schema.Jobs; public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob), 64), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob), ""), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string MangaId { get; init; } = mangaId; diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs index cecc901..257666f 100644 --- a/API/Schema/Jobs/DownloadSingleChapterJob.cs +++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs @@ -12,7 +12,7 @@ using static System.IO.UnixFileMode; namespace API.Schema.Jobs; public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), 64), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), ""), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string ChapterId { get; init; } = chapterId; diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs index a29d7b1..123ff92 100644 --- a/API/Schema/Jobs/MoveFileOrFolderJob.cs +++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs @@ -1,7 +1,7 @@ namespace API.Schema.Jobs; public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), 64), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), ""), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds) { public string FromLocation { get; init; } = fromLocation; public string ToLocation { get; init; } = toLocation; diff --git a/API/Schema/Jobs/UpdateMetadataJob.cs b/API/Schema/Jobs/UpdateMetadataJob.cs index c8d76cd..a04074b 100644 --- a/API/Schema/Jobs/UpdateMetadataJob.cs +++ b/API/Schema/Jobs/UpdateMetadataJob.cs @@ -1,9 +1,10 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; +using API.Schema.MangaConnectors; namespace API.Schema.Jobs; public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob), 64), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob), ""), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string MangaId { get; init; } = mangaId; diff --git a/API/Schema/LibraryConnectors/Kavita.cs b/API/Schema/LibraryConnectors/Kavita.cs index 51461c4..e87ebf8 100644 --- a/API/Schema/LibraryConnectors/Kavita.cs +++ b/API/Schema/LibraryConnectors/Kavita.cs @@ -6,7 +6,7 @@ namespace API.Schema.LibraryConnectors; public class Kavita : LibraryConnector { - public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), 64), LibraryType.Kavita, baseUrl, auth) + public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), baseUrl), LibraryType.Kavita, baseUrl, auth) { } diff --git a/API/Schema/LibraryConnectors/Komga.cs b/API/Schema/LibraryConnectors/Komga.cs index 36bc7ef..eb2a108 100644 --- a/API/Schema/LibraryConnectors/Komga.cs +++ b/API/Schema/LibraryConnectors/Komga.cs @@ -5,7 +5,7 @@ namespace API.Schema.LibraryConnectors; public class Komga : LibraryConnector { - public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), 64), LibraryType.Komga, + public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga, baseUrl, auth) { } diff --git a/API/Schema/Link.cs b/API/Schema/Link.cs index 9a439dd..fd415ad 100644 --- a/API/Schema/Link.cs +++ b/API/Schema/Link.cs @@ -7,7 +7,7 @@ namespace API.Schema; public class Link(string linkProvider, string linkUrl) { [MaxLength(64)] - public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), 64); + public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), string.Join(linkProvider, linkUrl)); public string LinkProvider { get; init; } = linkProvider; public string LinkUrl { get; init; } = linkUrl; diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs index fe510a8..543b885 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -14,7 +14,7 @@ namespace API.Schema; public class Manga { [MaxLength(64)] - public string MangaId { get; init; } = TokenGen.CreateToken(typeof(Manga), 64); + public string MangaId { get; init; } [MaxLength(64)] public string ConnectorId { get; init; } @@ -57,6 +57,7 @@ public class Manga string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, float ignoreChapterBefore, string mangaConnectorId) { + MangaId = TokenGen.CreateToken(typeof(Manga), string.Join(MangaConnectorId, ConnectorId)); ConnectorId = connectorId; Name = name; Description = description; diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs index 39d22aa..4c75834 100644 --- a/API/Schema/MangaAltTitle.cs +++ b/API/Schema/MangaAltTitle.cs @@ -8,7 +8,7 @@ namespace API.Schema; public class MangaAltTitle(string language, string title) { [MaxLength(64)] - public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", 64); + public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", string.Join(language, title)); [MaxLength(8)] public string Language { get; init; } = language; public string Title { get; set; } = title; diff --git a/API/Schema/MangaConnectors/AsuraToon.cs b/API/Schema/MangaConnectors/AsuraToon.cs index 4394200..b57db16 100644 --- a/API/Schema/MangaConnectors/AsuraToon.cs +++ b/API/Schema/MangaConnectors/AsuraToon.cs @@ -152,9 +152,7 @@ public class AsuraToon : MangaConnector string chapterUrl = chapterInfo.GetAttributeValue("href", ""); Match match = infoRex.Match(chapterInfo.InnerText); - if(!ChapterNumber.CanParse(match.Groups[1].Value)) - continue; - ChapterNumber chapterNumber = new(match.Groups[1].Value); + string chapterNumber = new(match.Groups[1].Value); string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null; string url = $"https://asuracomic.net/series/{chapterUrl}"; try diff --git a/API/Schema/MangaConnectors/Bato.cs b/API/Schema/MangaConnectors/Bato.cs index 80e3e03..a2e8dc3 100644 --- a/API/Schema/MangaConnectors/Bato.cs +++ b/API/Schema/MangaConnectors/Bato.cs @@ -159,9 +159,7 @@ public class Bato : MangaConnector Match match = numberRex.Match(chapterUrl); string id = match.Groups[1].Value; int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null; - if(ChapterNumber.CanParse(match.Groups[3].Value)) - continue; - ChapterNumber chapterNumber = new(match.Groups[3].Value); + string chapterNumber = new(match.Groups[3].Value); string url = $"https://bato.to{chapterUrl}?load=2"; try { diff --git a/API/Schema/MangaConnectors/MangaDex.cs b/API/Schema/MangaConnectors/MangaDex.cs index c4a4b42..a1abede 100644 --- a/API/Schema/MangaConnectors/MangaDex.cs +++ b/API/Schema/MangaConnectors/MangaDex.cs @@ -229,9 +229,7 @@ public class MangaDex : MangaConnector ? attributes["chapter"]!.GetValue() : null; - if(chapterNumStr is null || ChapterNumber.CanParse(chapterNumStr)) - continue; - ChapterNumber chapterNumber = new(chapterNumStr); + string chapterNumber = new(chapterNumStr); if (attributes.ContainsKey("pages") && attributes["pages"] is not null && diff --git a/API/Schema/MangaConnectors/MangaHere.cs b/API/Schema/MangaConnectors/MangaHere.cs index bf3ec40..4c7a6d4 100644 --- a/API/Schema/MangaConnectors/MangaHere.cs +++ b/API/Schema/MangaConnectors/MangaHere.cs @@ -128,9 +128,7 @@ public class MangaHere : MangaConnector Match rexMatch = chapterRex.Match(url); int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value); - if(!ChapterNumber.CanParse(rexMatch.Groups[2].Value)) - continue; - ChapterNumber chapterNumber = new(rexMatch.Groups[2].Value); + string chapterNumber = new(rexMatch.Groups[2].Value); string fullUrl = $"https://www.mangahere.cc{url}"; try diff --git a/API/Schema/MangaConnectors/MangaKatana.cs b/API/Schema/MangaConnectors/MangaKatana.cs index 7529988..0905db4 100644 --- a/API/Schema/MangaConnectors/MangaKatana.cs +++ b/API/Schema/MangaConnectors/MangaKatana.cs @@ -185,9 +185,8 @@ public class MangaKatana : MangaConnector .GetAttributeValue("href", ""); int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null; - if(!ChapterNumber.CanParse(chapterNumRex.Match(url).Groups[1].Value)) - continue; - ChapterNumber chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value); + + string chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value); string chapterName = chapterNameRex.Match(fullString).Groups[1].Value; try { diff --git a/API/Schema/MangaConnectors/MangaLife.cs b/API/Schema/MangaConnectors/MangaLife.cs index 30d72c6..54bdd1c 100644 --- a/API/Schema/MangaConnectors/MangaLife.cs +++ b/API/Schema/MangaConnectors/MangaLife.cs @@ -151,9 +151,8 @@ public class MangaLife : MangaConnector ? int.Parse(rexMatch.Groups[3].Value) : null; - if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value)) - continue; - ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value); + + string chapterNumber = new(rexMatch.Groups[1].Value); string fullUrl = $"https://manga4life.com{url}"; fullUrl = fullUrl.Replace(Regex.Match(url,"(-page-[0-9])").Value,""); try diff --git a/API/Schema/MangaConnectors/Manganato.cs b/API/Schema/MangaConnectors/Manganato.cs index 054dfc5..0ff9b8c 100644 --- a/API/Schema/MangaConnectors/Manganato.cs +++ b/API/Schema/MangaConnectors/Manganato.cs @@ -181,9 +181,7 @@ public class Manganato : MangaConnector int? volumeNumber = volRex.IsMatch(fullString) ? int.Parse(volRex.Match(fullString).Groups[1].Value) : null; - if(!ChapterNumber.CanParse(chapterRex.Match(url).Groups[1].Value)) - continue; - ChapterNumber chapterNumber = new(chapterRex.Match(url).Groups[1].Value); + string chapterNumber = new(chapterRex.Match(url).Groups[1].Value); string chapterName = nameRex.Match(fullString).Groups[3].Value; try { diff --git a/API/Schema/MangaConnectors/Mangaworld.cs b/API/Schema/MangaConnectors/Mangaworld.cs index cb0a81b..9e563e9 100644 --- a/API/Schema/MangaConnectors/Mangaworld.cs +++ b/API/Schema/MangaConnectors/Mangaworld.cs @@ -158,9 +158,8 @@ public class Mangaworld : MangaConnector { string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value; - if(!ChapterNumber.CanParse(numberStr)) - continue; - ChapterNumber chapterNumber = new(numberStr); + + string chapterNumber = new(numberStr); string url = chNode.SelectSingleNode("a").GetAttributeValue("href", ""); string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value; try @@ -178,9 +177,8 @@ public class Mangaworld : MangaConnector foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter"))) { string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value; - if(!ChapterNumber.CanParse(numberStr)) - continue; - ChapterNumber chapterNumber = new(numberStr); + + string chapterNumber = new(numberStr); string url = chNode.SelectSingleNode("a").GetAttributeValue("href", ""); string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value; try diff --git a/API/Schema/MangaConnectors/ManhuaPlus.cs b/API/Schema/MangaConnectors/ManhuaPlus.cs index 0312867..a8efb72 100644 --- a/API/Schema/MangaConnectors/ManhuaPlus.cs +++ b/API/Schema/MangaConnectors/ManhuaPlus.cs @@ -148,9 +148,7 @@ public class ManhuaPlus : MangaConnector { Match rexMatch = urlRex.Match(url); - if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value)) - continue; - ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value); + string chapterNumber = new(rexMatch.Groups[1].Value); string fullUrl = url; try { diff --git a/API/Schema/MangaConnectors/WeebCentral.cs b/API/Schema/MangaConnectors/WeebCentral.cs index 7568188..a329994 100644 --- a/API/Schema/MangaConnectors/WeebCentral.cs +++ b/API/Schema/MangaConnectors/WeebCentral.cs @@ -157,7 +157,7 @@ public class Weebcentral : MangaConnector public override Chapter[] GetChapters(Manga manga, string language = "en") { - var requestUrl = $"{_baseUrl}/series/{manga.MangaId}/full-chapter-list"; + var requestUrl = $"{_baseUrl}/series/{manga.ConnectorId}/full-chapter-list"; var requestResult = downloadClient.MakeRequest(requestUrl, RequestType.Default); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) @@ -182,7 +182,7 @@ public class Weebcentral : MangaConnector var url = elem.GetAttributeValue("href", "") ?? "Undefined"; if (!url.StartsWith("https://") && !url.StartsWith("http://")) - return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null); + return new Chapter(manga, "undefined", "-1", null, null); var idMatch = idRex.Match(url); var id = idMatch.Success ? idMatch.Groups[1].Value : null; @@ -192,12 +192,13 @@ public class Weebcentral : MangaConnector var chapterNumberMatch = chapterRex.Match(chapterNode); - if(!chapterNumberMatch.Success || !ChapterNumber.CanParse(chapterNumberMatch.Groups[1].Value)) - return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null); - ChapterNumber chapterNumber = new(chapterNumberMatch.Groups[1].Value); + if(!chapterNumberMatch.Success) + return new Chapter(manga, "undefined", "-1", null, null); - return new Chapter(manga, url, chapterNumber, null, null); - }).Where(elem => elem.ChapterNumber < ChapterNumber.Zero && elem.Url != "undefined").ToList(); + string chapterNumber = new(chapterNumberMatch.Groups[1].Value); + var chapter = new Chapter(manga, url, chapterNumber, null, null); + return chapter; + }).Where(elem => elem.ChapterNumber.CompareTo("-1") != 0 && elem.Url != "undefined").ToList(); ret.Reverse(); return ret; diff --git a/API/Schema/Notification.cs b/API/Schema/Notification.cs index 6b0a3f0..f913951 100644 --- a/API/Schema/Notification.cs +++ b/API/Schema/Notification.cs @@ -7,7 +7,7 @@ namespace API.Schema; public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null) { [MaxLength(64)] - public string NotificationId { get; init; } = TokenGen.CreateToken("Notification", 64); + public string NotificationId { get; init; } = TokenGen.CreateToken("Notification", ""); public NotificationUrgency Urgency { get; init; } = urgency; diff --git a/API/Schema/NotificationConnectors/Gotify.cs b/API/Schema/NotificationConnectors/Gotify.cs index 352fbda..ea1e6cc 100644 --- a/API/Schema/NotificationConnectors/Gotify.cs +++ b/API/Schema/NotificationConnectors/Gotify.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace API.Schema.NotificationConnectors; public class Gotify(string endpoint, string appToken) - : NotificationConnector(TokenGen.CreateToken(typeof(Gotify), 64), NotificationConnectorType.Gotify) + : NotificationConnector(TokenGen.CreateToken(typeof(Gotify), endpoint), NotificationConnectorType.Gotify) { public string Endpoint { get; init; } = endpoint; public string AppToken { get; init; } = appToken; diff --git a/API/Schema/NotificationConnectors/Lunasea.cs b/API/Schema/NotificationConnectors/Lunasea.cs index 498811d..da5cf1b 100644 --- a/API/Schema/NotificationConnectors/Lunasea.cs +++ b/API/Schema/NotificationConnectors/Lunasea.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace API.Schema.NotificationConnectors; public class Lunasea(string id) - : NotificationConnector(TokenGen.CreateToken(typeof(Lunasea), 64), NotificationConnectorType.LunaSea) + : NotificationConnector(TokenGen.CreateToken(typeof(Lunasea), id), NotificationConnectorType.LunaSea) { public string Id { get; init; } = id; public override void SendNotification(string title, string notificationText) diff --git a/API/Schema/NotificationConnectors/Ntfy.cs b/API/Schema/NotificationConnectors/Ntfy.cs index e95c6a5..ae7d3fb 100644 --- a/API/Schema/NotificationConnectors/Ntfy.cs +++ b/API/Schema/NotificationConnectors/Ntfy.cs @@ -11,7 +11,7 @@ public class Ntfy : NotificationConnector public string Auth { get; init; } public string Topic { get; init; } - public Ntfy(string endpoint, string auth, string topic): base(TokenGen.CreateToken(typeof(Ntfy), 64), NotificationConnectorType.Ntfy) + public Ntfy(string endpoint, string auth, string topic): base(TokenGen.CreateToken(typeof(Ntfy), endpoint), NotificationConnectorType.Ntfy) { Endpoint = endpoint; Auth = auth; diff --git a/API/TokenGen.cs b/API/TokenGen.cs index d7ca0da..947c482 100644 --- a/API/TokenGen.cs +++ b/API/TokenGen.cs @@ -5,36 +5,35 @@ namespace API; public static class TokenGen { - private const uint MinimumLength = 8; + private const int MinimumLength = 32; + private const int MaximumLength = 64; private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - public static string CreateToken(Type t, uint fullLength) => CreateToken(t.Name, fullLength); + public static string CreateToken(Type t, string identifier) => CreateToken(t.Name, identifier); - public static string CreateToken(string prefix, uint fullLength) + public static string CreateToken(string prefix, string identifier) { - if (prefix.Length + 1 >= fullLength - MinimumLength) + + + if (prefix.Length + 1 >= MaximumLength - MinimumLength) throw new ArgumentException("Prefix to long to create Token of meaningful length."); - long l = fullLength - prefix.Length - 1; - byte[] rng = new byte[l]; - RandomNumberGenerator.Create().GetBytes(rng); - string key = new (rng.Select(b => Chars[b % Chars.Length]).ToArray()); - key = string.Join('-', prefix, key); - return key; - } + + int tokenLength = MaximumLength - prefix.Length - 1; + + if (string.IsNullOrWhiteSpace(identifier)) + { + // No identifier, just create a random token + byte[] rng = new byte[tokenLength]; + RandomNumberGenerator.Create().GetBytes(rng); + string key = new(rng.Select(b => Chars[b % Chars.Length]).ToArray()); + key = string.Join('-', prefix, key); + return key; + } - public static string CreateTokenHash(string prefix, uint fullLength, string[] keys) - { - if (prefix.Length + 1 >= fullLength - MinimumLength) - throw new ArgumentException("Prefix to long to create Token of meaningful length."); - int l = (int)(fullLength - prefix.Length - 1); - MD5 md5 = MD5.Create(); - byte[][] hashes = keys.Select(key => md5.ComputeHash(Encoding.UTF8.GetBytes(key))).ToArray(); - byte[] xOrHash = new byte[l]; - foreach (byte[] hash in hashes) - for(int i = 0; i < hash.Length; i++) - xOrHash[i] = (byte)(xOrHash[i] ^ (i >= hash.Length ? 0 : hash[i])); - string key = new (xOrHash.Select(b => Chars[b % Chars.Length]).ToArray()); - key = string.Join('-', prefix, key); - return key; + // Identifier provided, create a token based on the identifier hashed + byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(identifier)); + string token = Convert.ToHexStringLower(hash); + + return string.Join('-', prefix, token); } } \ No newline at end of file From a713a006dd6c442457047ba5819f6c3318cf920f Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 25 Jan 2025 12:10:32 +0100 Subject: [PATCH 02/10] [postgres-Server-V2] fix: Updated Job logic to spawn a job only if a job with the same id is not already in the RunningJobs list --- API/Tranga.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/API/Tranga.cs b/API/Tranga.cs index e6d89a3..a9d9647 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -79,6 +79,9 @@ public static class Tranga List runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList().Where(j => j.NextExecution < DateTime.UtcNow).ToList(); foreach (Job job in runJobs) { + // If the job is already running, skip it + if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue; + Thread t = new (() => { IEnumerable newJobs = job.Run(context); From d6018b60ae43f1cd64dabfdc6d63e4968bb185c7 Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Fri, 31 Jan 2025 21:24:35 +0100 Subject: [PATCH 03/10] [postgres-Server-V2] Add parameter array to token gen --- API/Schema/Chapter.cs | 2 +- API/Schema/Jobs/DownloadMangaCoverJob.cs | 2 +- API/Schema/Jobs/DownloadNewChaptersJob.cs | 2 +- API/Schema/Jobs/DownloadSingleChapterJob.cs | 2 +- API/Schema/Jobs/MoveFileOrFolderJob.cs | 2 +- API/Schema/Jobs/UpdateMetadataJob.cs | 2 +- API/Schema/Link.cs | 2 +- API/Schema/Manga.cs | 4 ++-- API/Schema/MangaAltTitle.cs | 2 +- API/Schema/Notification.cs | 2 +- API/TokenGen.cs | 8 ++++---- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs index 5c9ce27..46771c0 100644 --- a/API/Schema/Chapter.cs +++ b/API/Schema/Chapter.cs @@ -18,7 +18,7 @@ public class Chapter : IComparable public Chapter(string parentMangaId, string url, string chapterNumber, int? volumeNumber = null, string? title = null) { - ChapterId = TokenGen.CreateToken(typeof(Chapter), string.Join(parentMangaId, volumeNumber ?? 0, chapterNumber)); + ChapterId = TokenGen.CreateToken(typeof(Chapter), parentMangaId, (volumeNumber ?? 0).ToString(), chapterNumber); ParentMangaId = parentMangaId; Url = url; ChapterNumber = chapterNumber; diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs index e6ef6d0..56e630a 100644 --- a/API/Schema/Jobs/DownloadMangaCoverJob.cs +++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs @@ -12,7 +12,7 @@ using static System.IO.UnixFileMode; namespace API.Schema.Jobs; public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob), ""), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string ChapterId { get; init; } = chapterId; diff --git a/API/Schema/Jobs/DownloadNewChaptersJob.cs b/API/Schema/Jobs/DownloadNewChaptersJob.cs index b090d0b..6cc82b8 100644 --- a/API/Schema/Jobs/DownloadNewChaptersJob.cs +++ b/API/Schema/Jobs/DownloadNewChaptersJob.cs @@ -4,7 +4,7 @@ using API.Schema.MangaConnectors; namespace API.Schema.Jobs; public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob), ""), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob)), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string MangaId { get; init; } = mangaId; diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs index 257666f..e6cee60 100644 --- a/API/Schema/Jobs/DownloadSingleChapterJob.cs +++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs @@ -12,7 +12,7 @@ using static System.IO.UnixFileMode; namespace API.Schema.Jobs; public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), ""), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string ChapterId { get; init; } = chapterId; diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs index 123ff92..956dd7c 100644 --- a/API/Schema/Jobs/MoveFileOrFolderJob.cs +++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs @@ -1,7 +1,7 @@ namespace API.Schema.Jobs; public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), ""), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds) { public string FromLocation { get; init; } = fromLocation; public string ToLocation { get; init; } = toLocation; diff --git a/API/Schema/Jobs/UpdateMetadataJob.cs b/API/Schema/Jobs/UpdateMetadataJob.cs index a04074b..c5706e8 100644 --- a/API/Schema/Jobs/UpdateMetadataJob.cs +++ b/API/Schema/Jobs/UpdateMetadataJob.cs @@ -4,7 +4,7 @@ using API.Schema.MangaConnectors; namespace API.Schema.Jobs; public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob), ""), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds) + : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob)), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string MangaId { get; init; } = mangaId; diff --git a/API/Schema/Link.cs b/API/Schema/Link.cs index fd415ad..af08155 100644 --- a/API/Schema/Link.cs +++ b/API/Schema/Link.cs @@ -7,7 +7,7 @@ namespace API.Schema; public class Link(string linkProvider, string linkUrl) { [MaxLength(64)] - public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), string.Join(linkProvider, linkUrl)); + public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl); public string LinkProvider { get; init; } = linkProvider; public string LinkUrl { get; init; } = linkUrl; diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs index 543b885..6b58fa9 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -57,7 +57,7 @@ public class Manga string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, float ignoreChapterBefore, string mangaConnectorId) { - MangaId = TokenGen.CreateToken(typeof(Manga), string.Join(MangaConnectorId, ConnectorId)); + MangaId = TokenGen.CreateToken(typeof(Manga), MangaConnectorId, ConnectorId); ConnectorId = connectorId; Name = name; Description = description; diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs index 4c75834..54a8144 100644 --- a/API/Schema/MangaAltTitle.cs +++ b/API/Schema/MangaAltTitle.cs @@ -8,7 +8,7 @@ namespace API.Schema; public class MangaAltTitle(string language, string title) { [MaxLength(64)] - public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", string.Join(language, title)); + public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", language, title); [MaxLength(8)] public string Language { get; init; } = language; public string Title { get; set; } = title; diff --git a/API/Schema/Notification.cs b/API/Schema/Notification.cs index f913951..959b2e8 100644 --- a/API/Schema/Notification.cs +++ b/API/Schema/Notification.cs @@ -7,7 +7,7 @@ namespace API.Schema; public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null) { [MaxLength(64)] - public string NotificationId { get; init; } = TokenGen.CreateToken("Notification", ""); + public string NotificationId { get; init; } = TokenGen.CreateToken("Notification"); public NotificationUrgency Urgency { get; init; } = urgency; diff --git a/API/TokenGen.cs b/API/TokenGen.cs index 947c482..3f3d2ae 100644 --- a/API/TokenGen.cs +++ b/API/TokenGen.cs @@ -9,9 +9,9 @@ public static class TokenGen private const int MaximumLength = 64; private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - public static string CreateToken(Type t, string identifier) => CreateToken(t.Name, identifier); + public static string CreateToken(Type t, params string[] identifiers) => CreateToken(t.Name, identifiers); - public static string CreateToken(string prefix, string identifier) + public static string CreateToken(string prefix, params string[] identifiers) { @@ -20,7 +20,7 @@ public static class TokenGen int tokenLength = MaximumLength - prefix.Length - 1; - if (string.IsNullOrWhiteSpace(identifier)) + if (identifiers.Length == 0) { // No identifier, just create a random token byte[] rng = new byte[tokenLength]; @@ -31,7 +31,7 @@ public static class TokenGen } // Identifier provided, create a token based on the identifier hashed - byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(identifier)); + byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(string.Join("", identifiers))); string token = Convert.ToHexStringLower(hash); return string.Join('-', prefix, token); From 6bbd09072b6a1be766cb2417ec52c0d74ddb5b42 Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 1 Feb 2025 21:52:34 +0100 Subject: [PATCH 04/10] [postgres-Server-V2] fix(Chapter): Use tryParse instead of parse when comparing chapters --- API/Schema/Chapter.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs index 46771c0..97cf2f9 100644 --- a/API/Schema/Chapter.cs +++ b/API/Schema/Chapter.cs @@ -80,8 +80,7 @@ public class Chapter : IComparable { string oldPath = GetArchiveFilePath(); ArchiveFileName = BuildArchiveFileName(); - if (Downloaded) return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath()); - return null; + return Downloaded ? new MoveFileOrFolderJob(oldPath, GetArchiveFilePath()) : null; } /// @@ -101,9 +100,12 @@ public class Chapter : IComparable private static int CompareChapterNumbers(string ch1, string ch2) { - int[] ch1Arr = ch1.Split('.').Select(c => int.Parse(c)).ToArray(); - int[] ch2Arr = ch2.Split('.').Select(c => int.Parse(c)).ToArray(); - + int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray(); + int[] ch2Arr = ch2.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray(); + + if (ch1Arr.Contains(-1) || ch2Arr.Contains(-1)) + throw new ArgumentException("Chapter number is not in correct format"); + int i = 0, j = 0; while (i < ch1Arr.Length && j < ch2Arr.Length) From bd9e79d026292036cbab8f752d9b30c14500c695 Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 1 Feb 2025 21:53:16 +0100 Subject: [PATCH 05/10] [postgres-Server-V2] Use MangaId to check if manga exists instead of connector id --- API/Controllers/SearchController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 3b92bf0..3c45c8d 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -1,4 +1,4 @@ -using API.Schema; +using API.Schema; using API.Schema.MangaConnectors; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; @@ -82,8 +82,9 @@ public class SearchController(PgsqlContext context) : Controller { if (manga is null) return null; + Manga? existing = context.Manga.FirstOrDefault(m => - m.MangaConnector == manga.MangaConnector && m.ConnectorId == manga.ConnectorId); + m.MangaId == manga.MangaId); if (tags is not null) { From 45ca2695eb9331d76088d7ffb122ea8306bb802c Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 1 Feb 2025 21:58:50 +0100 Subject: [PATCH 06/10] [postgres-Server-V2] feat: Convert DateTime.Now calls to UtcNow --- API/MangaDownloadClients/DownloadClient.cs | 6 +++--- API/Schema/Jobs/DownloadMangaCoverJob.cs | 1 - API/Schema/Jobs/DownloadSingleChapterJob.cs | 2 +- API/Schema/Manga.cs | 3 ++- API/Schema/MangaConnectors/Bato.cs | 2 +- API/Schema/MangaConnectors/MangaKatana.cs | 2 +- API/Tranga.cs | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/API/MangaDownloadClients/DownloadClient.cs b/API/MangaDownloadClients/DownloadClient.cs index 9dcd049..d930852 100644 --- a/API/MangaDownloadClients/DownloadClient.cs +++ b/API/MangaDownloadClients/DownloadClient.cs @@ -24,9 +24,9 @@ internal abstract class DownloadClient : TrangaSettings.requestLimits[requestType]; TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit); - _lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(timeBetweenRequests)); + _lastExecutedRateLimit.TryAdd(requestType, DateTime.UtcNow.Subtract(timeBetweenRequests)); - TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType])); + TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.UtcNow.Subtract(_lastExecutedRateLimit[requestType])); if (rateLimitTimeout > TimeSpan.Zero) { @@ -34,7 +34,7 @@ internal abstract class DownloadClient } RequestResult result = MakeRequestInternal(url, referrer, clickButton); - _lastExecutedRateLimit[requestType] = DateTime.Now; + _lastExecutedRateLimit[requestType] = DateTime.UtcNow; return result; } diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs index 56e630a..4c6403a 100644 --- a/API/Schema/Jobs/DownloadMangaCoverJob.cs +++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs @@ -79,7 +79,6 @@ public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, { if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) return; - DateTime start = DateTime.Now; using Image image = Image.Load(imagePath); File.Delete(imagePath); if(TrangaSettings.bwImages) diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs index e6cee60..81ad66a 100644 --- a/API/Schema/Jobs/DownloadSingleChapterJob.cs +++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs @@ -81,7 +81,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu { if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) return; - DateTime start = DateTime.Now; + DateTime start = DateTime.UtcNow; using Image image = Image.Load(imagePath); File.Delete(imagePath); if(TrangaSettings.bwImages) diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs index 6b58fa9..1055142 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -30,6 +30,7 @@ public class Manga public float IgnoreChapterBefore { get; internal set; } public string MangaConnectorId { get; private set; } + public MangaConnector? MangaConnector { get; private set; } public ICollection? Authors { get; internal set; } diff --git a/API/Schema/MangaConnectors/Bato.cs b/API/Schema/MangaConnectors/Bato.cs index a2e8dc3..1d98e0e 100644 --- a/API/Schema/MangaConnectors/Bato.cs +++ b/API/Schema/MangaConnectors/Bato.cs @@ -97,7 +97,7 @@ public class Bato : MangaConnector if (!uint.TryParse( document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0], out uint year)) - year = (uint)DateTime.Now.Year; + year = (uint)DateTime.UtcNow.Year; string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..") .ChildNodes[2].InnerText; diff --git a/API/Schema/MangaConnectors/MangaKatana.cs b/API/Schema/MangaConnectors/MangaKatana.cs index 0905db4..3bc77a3 100644 --- a/API/Schema/MangaConnectors/MangaKatana.cs +++ b/API/Schema/MangaConnectors/MangaKatana.cs @@ -127,7 +127,7 @@ public class MangaKatana : MangaConnector while (description.StartsWith('\n')) description = description.Substring(1); - uint year = (uint)DateTime.Now.Year; + uint year = (uint)DateTime.UtcNow.Year; string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt")) .InnerText.Split('-')[^1]; diff --git a/API/Tranga.cs b/API/Tranga.cs index a9d9647..4e958b1 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -43,7 +43,7 @@ public static class Tranga if (notifications.Any()) { DateTime max = notifications.MaxBy(n => n.Date)!.Date; - if (DateTime.Now.Subtract(max) > TrangaSettings.NotificationUrgencyDelay(urgency)) + if (DateTime.UtcNow.Subtract(max) > TrangaSettings.NotificationUrgencyDelay(urgency)) { foreach (NotificationConnector notificationConnector in context.NotificationConnectors) { From a69e12179b57512b948565d44f4a40507087739e Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 1 Feb 2025 22:16:12 +0100 Subject: [PATCH 07/10] [postgres-Server-V2] fix(Manga): Was using properties before initialization --- API/Schema/Manga.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs index 1055142..0296bbd 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -58,7 +58,7 @@ public class Manga string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, float ignoreChapterBefore, string mangaConnectorId) { - MangaId = TokenGen.CreateToken(typeof(Manga), MangaConnectorId, ConnectorId); + MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, connectorId); ConnectorId = connectorId; Name = name; Description = description; From 725813c2f390945362c02a96fa89d2f967ae9a20 Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sat, 1 Feb 2025 23:08:13 +0100 Subject: [PATCH 08/10] [postgres-Server-V2] feat(Jobs): Instead of passing context to each job, pass service provider and let the job create its own context --- API/Program.cs | 2 +- API/Schema/Jobs/Job.cs | 5 ++++- API/Tranga.cs | 29 +++++++++++++++++------------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/API/Program.cs b/API/Program.cs index 983853b..e904eac 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -124,7 +124,7 @@ using (var scope = app.Services.CreateScope()) TrangaSettings.Load(); Tranga.StartLogger(); -Tranga.JobStarterThread.Start(app.Services.CreateScope().ServiceProvider.GetService()); +Tranga.JobStarterThread.Start(app.Services); Tranga.NotificationSenderThread.Start(app.Services.CreateScope().ServiceProvider.GetService()); app.UseCors("AllowAll"); diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs index 16553d2..e5c078e 100644 --- a/API/Schema/Jobs/Job.cs +++ b/API/Schema/Jobs/Job.cs @@ -43,8 +43,11 @@ public abstract class Job RecurrenceMs = recurrenceMs; } - public IEnumerable Run(PgsqlContext context) + public IEnumerable Run(IServiceProvider serviceProvider) { + using IServiceScope scope = serviceProvider.CreateScope(); + PgsqlContext context = scope.ServiceProvider.GetRequiredService(); + this.state = JobState.Running; IEnumerable newJobs = RunInternal(context); this.state = JobState.Completed; diff --git a/API/Tranga.cs b/API/Tranga.cs index 4e958b1..e494fb3 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -56,18 +56,22 @@ public static class Tranga context.SaveChanges(); } - private static void JobStarter(object? pgsqlContext) + private static void JobStarter(object? serviceProviderObj) { - if(pgsqlContext is null) return; - PgsqlContext context = (PgsqlContext)pgsqlContext; - - string TRANGA = "\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n"; + if(serviceProviderObj is null) return; + IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj; + using IServiceScope scope = serviceProvider.CreateScope(); + PgsqlContext? context = scope.ServiceProvider.GetService(); + if (context is null) return; + + string TRANGA = + "\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n"; Log.Info(TRANGA); while (true) { List completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList(); foreach (Job job in completedJobs) - if(job.RecurrenceMs <= 0) + if (job.RecurrenceMs <= 0) context.Jobs.Remove(job); else { @@ -75,16 +79,17 @@ public static class Tranga job.state = JobState.Waiting; context.Jobs.Update(job); } - - List runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList().Where(j => j.NextExecution < DateTime.UtcNow).ToList(); + + List runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList() + .Where(j => j.NextExecution < DateTime.UtcNow).ToList(); foreach (Job job in runJobs) { // If the job is already running, skip it if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue; - - Thread t = new (() => + + Thread t = new(() => { - IEnumerable newJobs = job.Run(context); + IEnumerable newJobs = job.Run(serviceProvider); context.Jobs.AddRange(newJobs); }); RunningJobs.Add(t, job); @@ -99,7 +104,7 @@ public static class Tranga RunningJobs.Remove(thread.thread); context.Jobs.Update(thread.job); } - + context.SaveChanges(); Thread.Sleep(2000); } From ef7ebf022d5361f30670a4d4173cd135cdd788ad Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sun, 2 Feb 2025 00:18:38 +0100 Subject: [PATCH 09/10] [postgres-Server-V2] Update Weebcentral with changes from V1 --- API/Schema/MangaConnectors/WeebCentral.cs | 141 +++++++++++----------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/API/Schema/MangaConnectors/WeebCentral.cs b/API/Schema/MangaConnectors/WeebCentral.cs index a329994..aa1e6a9 100644 --- a/API/Schema/MangaConnectors/WeebCentral.cs +++ b/API/Schema/MangaConnectors/WeebCentral.cs @@ -17,37 +17,38 @@ public class Weebcentral : MangaConnector downloadClient = new ChromiumDownloadClient(); } - public override (Manga, List?, List?, List?, List?)[] GetManga(string publicationTitle = "") + public override (Manga, List?, List?, List?, List?)[] GetManga( + string publicationTitle = "") { const int limit = 32; //How many values we want returned at once - var offset = 0; //"Page" - var requestUrl = + int offset = 0; //"Page" + string requestUrl = $"{_baseUrl}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display"; - var requestResult = + RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.Default); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument == null) - { return []; - } - var publications = ParsePublicationsFromHtml(requestResult.htmlDocument); + (Manga, List, List, List, List)[] publications = + ParsePublicationsFromHtml(requestResult.htmlDocument); return publications; } - private (Manga, List?, List?, List?, List?)[] ParsePublicationsFromHtml(HtmlDocument document) + private (Manga, List?, List?, List?, List?)[] ParsePublicationsFromHtml( + HtmlDocument document) { if (document.DocumentNode.SelectNodes("//article") == null) return []; - var urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']") + List urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']") .Select(elem => elem.GetAttributeValue("href", "")).ToList(); List<(Manga, List?, List?, List?, List?)> ret = new(); - foreach (var url in urls) + foreach (string url in urls) { - var manga = GetMangaFromUrl(url); + (Manga, List, List, List, List)? manga = GetMangaFromUrl(url); if (manga is { } x) ret.Add(x); } @@ -55,30 +56,32 @@ public class Weebcentral : MangaConnector return ret.ToArray(); } - public override (Manga, List?, List?, List?, List?)? GetMangaFromUrl(string url) + public override (Manga, List?, List?, List?, List?)? + GetMangaFromUrl(string url) { Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)"); - var publicationId = publicationIdRex.Match(url).Groups[1].Value; + string publicationId = publicationIdRex.Match(url).Groups[1].Value; - var requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo); + RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo); if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null) return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url); return null; } - private (Manga, List?, List?, List?, List?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) + private (Manga, List?, List?, List?, List?) ParseSinglePublicationFromHtml( + HtmlDocument document, string publicationId, string websiteUrl) { - var posterNode = + HtmlNode? posterNode = document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img"); - var coverUrl = posterNode?.GetAttributeValue("src", "") ?? ""; + string coverUrl = posterNode?.GetAttributeValue("src", "") ?? ""; - var titleNode = document.DocumentNode.SelectSingleNode("//section/h1"); - var sortName = titleNode?.InnerText ?? "Undefined"; + HtmlNode? titleNode = document.DocumentNode.SelectSingleNode("//section/h1"); + string sortName = titleNode?.InnerText ?? "Undefined"; HtmlNode[] authorsNodes = document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span")?.ToArray() ?? []; - var authorNames = authorsNodes.Select(n => n.InnerText).ToList(); + List authorNames = authorsNodes.Select(n => n.InnerText).ToList(); List authors = authorNames.Select(n => new Author(n)).ToList(); HtmlNode[] genreNodes = @@ -86,9 +89,9 @@ public class Weebcentral : MangaConnector HashSet tags = genreNodes.Select(n => n.InnerText).ToHashSet(); List mangaTags = tags.Select(t => new MangaTag(t)).ToList(); - var statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a"); - var status = statusNode?.InnerText ?? ""; - var releaseStatus = MangaReleaseStatus.Unreleased; + HtmlNode? statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a"); + string status = statusNode?.InnerText ?? ""; + MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased; switch (status.ToLower()) { case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break; @@ -97,33 +100,34 @@ public class Weebcentral : MangaConnector case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break; } - var yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span"); - var year = uint.Parse(yearNode?.InnerText ?? "0"); + HtmlNode? yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span"); + uint year = uint.Parse(yearNode?.InnerText ?? "0"); - var descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p"); - var description = descriptionNode?.InnerText ?? "Undefined"; + HtmlNode? descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p"); + string description = descriptionNode?.InnerText ?? "Undefined"; HtmlNode[] altTitleNodes = document.DocumentNode .SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? []; Dictionary altTitlesDict = new(), links = new(); - for (var i = 0; i < altTitleNodes.Length; i++) + for (int i = 0; i < altTitleNodes.Length; i++) altTitlesDict.Add(i.ToString(), altTitleNodes[i].InnerText); List altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList(); - var originalLanguage = ""; + string originalLanguage = ""; - Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year, + Manga manga = new(publicationId, sortName, description, websiteUrl, coverUrl, null, year, originalLanguage, releaseStatus, -1, - this, - authors, - mangaTags, + this, + authors, + mangaTags, [], altTitles); - + return (manga, authors, mangaTags, [], altTitles); } - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) + public override (Manga, List?, List?, List?, List?)? GetMangaFromId( + string publicationId) { return GetMangaFromUrl($"https://weebcentral.com/series/{publicationId}"); } @@ -136,68 +140,67 @@ public class Weebcentral : MangaConnector private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults) { Dictionary similarity = new(); - foreach (var sr in unfilteredSearchResults) + foreach (SearchResult sr in unfilteredSearchResults) { List scores = new(); - var filteredPublicationString = ToFilteredString(publicationTitle); - var filteredSString = ToFilteredString(sr.s); + string filteredPublicationString = ToFilteredString(publicationTitle); + string filteredSString = ToFilteredString(sr.s); scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredSString, filteredPublicationString)); - foreach (var srA in sr.a) + foreach (string srA in sr.a) { - var filteredAString = ToFilteredString(srA); + string filteredAString = ToFilteredString(srA); scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredAString, filteredPublicationString)); } similarity.Add(sr, scores.Sum() / scores.Count); } - var ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList(); + List ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList(); return ret.ToArray(); } public override Chapter[] GetChapters(Manga manga, string language = "en") { - var requestUrl = $"{_baseUrl}/series/{manga.ConnectorId}/full-chapter-list"; - var requestResult = + string requestUrl = $"{_baseUrl}/series/{manga.ConnectorId}/full-chapter-list"; + RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.Default); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return Array.Empty(); + return []; //Return Chapters ordered by Chapter-Number if (requestResult.htmlDocument is null) - return Array.Empty(); - var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument); + return []; + List chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument); return chapters.Order().ToArray(); } private List ParseChaptersFromHtml(Manga manga, HtmlDocument document) { - var chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body"); + HtmlNode? chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body"); - Regex chapterRex = new(@".* (\d+)"); + Regex chapterRex = new(@"(\d+(?:\.\d+)*)"); Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)"); - var ret = chaptersWrapper.Descendants("a").Select(elem => + List ret = chaptersWrapper.Descendants("a").Select(elem => { - var url = elem.GetAttributeValue("href", "") ?? "Undefined"; + string url = elem.GetAttributeValue("href", "") ?? "Undefined"; if (!url.StartsWith("https://") && !url.StartsWith("http://")) - return new Chapter(manga, "undefined", "-1", null, null); + return new Chapter(manga, "undefined", "-1"); - var idMatch = idRex.Match(url); - var id = idMatch.Success ? idMatch.Groups[1].Value : null; + Match idMatch = idRex.Match(url); + string? id = idMatch.Success ? idMatch.Groups[1].Value : null; - var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ?? - "Undefined"; + string chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ?? + "Undefined"; - var chapterNumberMatch = chapterRex.Match(chapterNode); + Match chapterNumberMatch = chapterRex.Match(chapterNode); - if(!chapterNumberMatch.Success) - return new Chapter(manga, "undefined", "-1", null, null); - - string chapterNumber = new(chapterNumberMatch.Groups[1].Value); - var chapter = new Chapter(manga, url, chapterNumber, null, null); - return chapter; + if (!chapterNumberMatch.Success) + return new Chapter(manga, "undefined", "-1"); + + string chapterNumber = chapterNumberMatch.Groups[1].Value; + return new Chapter(manga, url, chapterNumber); }).Where(elem => elem.ChapterNumber.CompareTo("-1") != 0 && elem.Url != "undefined").ToList(); ret.Reverse(); @@ -206,17 +209,15 @@ public class Weebcentral : MangaConnector internal override string[] GetChapterImageUrls(Chapter chapter) { - var requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||requestResult.htmlDocument is null) - { - return []; - } + RequestResult requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default); + if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || + requestResult.htmlDocument is null) return []; - var document = requestResult.htmlDocument; + HtmlDocument? document = requestResult.htmlDocument; - var imageNodes = + HtmlNode[] imageNodes = document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.Url}/images']/img")?.ToArray() ?? []; - var urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray(); + string[] urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray(); return urls; } From 6315940cd6fc6567e1822c37e1bd5d3a3119f021 Mon Sep 17 00:00:00 2001 From: Alessandro Benetton Date: Sun, 2 Feb 2025 00:18:57 +0100 Subject: [PATCH 10/10] [postgres-Server-V2] fix: pk conflict error in chapter download (still not working though) --- API/Schema/Jobs/DownloadNewChaptersJob.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/API/Schema/Jobs/DownloadNewChaptersJob.cs b/API/Schema/Jobs/DownloadNewChaptersJob.cs index 6cc82b8..b00472e 100644 --- a/API/Schema/Jobs/DownloadNewChaptersJob.cs +++ b/API/Schema/Jobs/DownloadNewChaptersJob.cs @@ -12,11 +12,22 @@ public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? protected override IEnumerable RunInternal(PgsqlContext context) { - Manga m = Manga ?? context.Manga.Find(MangaId)!; - MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!; - Chapter[] newChapters = connector.GetNewChapters(m); + /* + * For some reason, directly using Manga from above instead of finding it again causes DBContext to consider + * Manga as a new entity and Postgres throws a Duplicate PK exception. + * m.MangaConnector does not have this issue (IDK why). + */ + Manga m = context.Manga.Find(MangaId)!; + MangaConnector connector = context.MangaConnectors.Find(m.MangaConnectorId)!; + // This gets all chapters that are not downloaded + Chapter[] allNewChapters = connector.GetNewChapters(m); + + // This filters out chapters that are not downloaded but already exist in the DB + string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == m.MangaId).Select(chapter => chapter.ChapterId).ToArray(); + Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray(); context.Chapters.AddRangeAsync(newChapters).Wait(); context.SaveChangesAsync().Wait(); - return newChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId)); + + return allNewChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId)); } } \ No newline at end of file