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)
{
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/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/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/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..97cf2f9 100644
--- a/API/Schema/Chapter.cs
+++ b/API/Schema/Chapter.cs
@@ -8,71 +8,83 @@ 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), parentMangaId, (volumeNumber ?? 0).ToString(), 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());
- }
- return null;
+ ArchiveFileName = BuildArchiveFileName();
+ return Downloaded ? new MoveFileOrFolderJob(oldPath, GetArchiveFilePath()) : null;
}
-
+
///
- /// Creates full file path of chapter-archive
+ /// Creates full file path of chapter-archive
///
/// Filepath
internal string GetArchiveFilePath()
@@ -86,27 +98,38 @@ 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.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)
{
- <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..4c6403a 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;
@@ -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/DownloadNewChaptersJob.cs b/API/Schema/Jobs/DownloadNewChaptersJob.cs
index 241a051..b00472e 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;
@@ -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
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index cecc901..81ad66a 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;
@@ -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/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/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs
index a29d7b1..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), 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..c5706e8 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..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), 64);
+ 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 fe510a8..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;
@@ -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; }
@@ -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; }
@@ -57,6 +58,7 @@ public class Manga
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
float ignoreChapterBefore, string mangaConnectorId)
{
+ 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 39d22aa..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", 64);
+ 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/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..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;
@@ -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..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];
@@ -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..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,68 @@ 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.MangaId}/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", new ChapterNumber(-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 || !ChapterNumber.CanParse(chapterNumberMatch.Groups[1].Value))
- return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null);
- ChapterNumber chapterNumber = new(chapterNumberMatch.Groups[1].Value);
-
- return new Chapter(manga, url, chapterNumber, null, null);
- }).Where(elem => elem.ChapterNumber < ChapterNumber.Zero && elem.Url != "undefined").ToList();
+ 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();
return ret;
@@ -205,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;
}
diff --git a/API/Schema/Notification.cs b/API/Schema/Notification.cs
index 6b0a3f0..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", 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..3f3d2ae 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, params string[] identifiers) => CreateToken(t.Name, identifiers);
- public static string CreateToken(string prefix, uint fullLength)
+ public static string CreateToken(string prefix, params string[] identifiers)
{
- 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 (identifiers.Length == 0)
+ {
+ // 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(string.Join("", identifiers)));
+ string token = Convert.ToHexStringLower(hash);
+
+ return string.Join('-', prefix, token);
}
}
\ No newline at end of file
diff --git a/API/Tranga.cs b/API/Tranga.cs
index e6d89a3..e494fb3 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)
{
@@ -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,13 +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)
{
- Thread t = new (() =>
+ // 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);
+ IEnumerable newJobs = job.Run(serviceProvider);
context.Jobs.AddRange(newJobs);
});
RunningJobs.Add(t, job);
@@ -96,7 +104,7 @@ public static class Tranga
RunningJobs.Remove(thread.thread);
context.Jobs.Update(thread.job);
}
-
+
context.SaveChanges();
Thread.Sleep(2000);
}