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