Merge pull request #329 from ale-ben/postgres-Server-V2

[Server V2] Changed chapterNumber to string and fixed job spawning error
This commit is contained in:
Glax 2025-02-09 17:20:19 +01:00 committed by GitHub
commit 663e2e2ca0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 999 additions and 522 deletions

View File

@ -1,4 +1,4 @@
using API.Schema; using API.Schema;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -82,8 +82,9 @@ public class SearchController(PgsqlContext context) : Controller
{ {
if (manga is null) if (manga is null)
return null; return null;
Manga? existing = context.Manga.FirstOrDefault(m => Manga? existing = context.Manga.FirstOrDefault(m =>
m.MangaConnector == manga.MangaConnector && m.ConnectorId == manga.ConnectorId); m.MangaId == manga.MangaId);
if (tags is not null) if (tags is not null)
{ {

View File

@ -24,9 +24,9 @@ internal abstract class DownloadClient
: TrangaSettings.requestLimits[requestType]; : TrangaSettings.requestLimits[requestType];
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit); 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) if (rateLimitTimeout > TimeSpan.Zero)
{ {
@ -34,7 +34,7 @@ internal abstract class DownloadClient
} }
RequestResult result = MakeRequestInternal(url, referrer, clickButton); RequestResult result = MakeRequestInternal(url, referrer, clickButton);
_lastExecutedRateLimit[requestType] = DateTime.Now; _lastExecutedRateLimit[requestType] = DateTime.UtcNow;
return result; return result;
} }

View File

@ -0,0 +1,693 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasColumnType("text");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ArchiveFileName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<string>("JobId1")
.HasColumnType("character varying(64)");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<int>("state")
.HasColumnType("integer");
b.HasKey("JobId");
b.HasIndex("JobId1");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("LibraryConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BaseUrl")
.IsRequired()
.HasColumnType("text");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("LibraryConnectorId");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Link");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FolderName")
.IsRequired()
.HasColumnType("text");
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OriginalLanguage")
.HasColumnType("text");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasColumnType("text");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("MangaConnectorId");
b.ToTable("Manga");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnectors");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaTag", b =>
{
b.Property<string>("Tag")
.HasColumnType("text");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("NotificationId");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("NotificationConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<byte>("NotificationConnectorType")
.HasColumnType("smallint");
b.HasKey("NotificationConnectorId");
b.ToTable("NotificationConnectors");
b.HasDiscriminator<byte>("NotificationConnectorType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorManga", b =>
{
b.Property<string>("AuthorsAuthorId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId");
b.HasIndex("MangaId");
b.ToTable("AuthorManga");
});
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("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<string>("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<string>("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<string>("FromLocation")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ToLocation")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("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<string>("AppToken")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.NotificationConnectors.Lunasea", b =>
{
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
b.Property<string>("Id")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.NotificationConnectors.Ntfy", b =>
{
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("text");
b.Property<string>("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
}
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class ChapterNumber : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "VolumeNumber",
table: "Chapters",
type: "integer",
nullable: true,
oldClrType: typeof(float),
oldType: "real",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ChapterNumber",
table: "Chapters",
type: "character varying(10)",
maxLength: 10,
nullable: false,
oldClrType: typeof(float),
oldType: "real");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<float>(
name: "VolumeNumber",
table: "Chapters",
type: "real",
nullable: true,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<float>(
name: "ChapterNumber",
table: "Chapters",
type: "real",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(10)",
oldMaxLength: 10);
}
}
}

View File

@ -47,8 +47,10 @@ namespace API.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<float>("ChapterNumber") b.Property<string>("ChapterNumber")
.HasColumnType("real"); .IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded") b.Property<bool>("Downloaded")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -64,8 +66,8 @@ namespace API.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<float?>("VolumeNumber") b.Property<int?>("VolumeNumber")
.HasColumnType("real"); .HasColumnType("integer");
b.HasKey("ChapterId"); b.HasKey("ChapterId");

View File

@ -124,7 +124,7 @@ using (var scope = app.Services.CreateScope())
TrangaSettings.Load(); TrangaSettings.Load();
Tranga.StartLogger(); Tranga.StartLogger();
Tranga.JobStarterThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>()); Tranga.JobStarterThread.Start(app.Services);
Tranga.NotificationSenderThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>()); Tranga.NotificationSenderThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>());
app.UseCors("AllowAll"); app.UseCors("AllowAll");

View File

@ -7,6 +7,6 @@ namespace API.Schema;
public class Author(string authorName) public class Author(string authorName)
{ {
[MaxLength(64)] [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; public string AuthorName { get; init; } = authorName;
} }

View File

@ -8,10 +8,30 @@ namespace API.Schema;
[PrimaryKey("ChapterId")] [PrimaryKey("ChapterId")]
public class Chapter : IComparable<Chapter> public class Chapter : IComparable<Chapter>
{ {
[MaxLength(64)] public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
public string ChapterId { get; init; } = TokenGen.CreateToken(typeof(Chapter), 64); : 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 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 Url { get; internal set; }
public string? Title { get; private set; } public string? Title { get; private set; }
public string ArchiveFileName { get; private set; } public string ArchiveFileName { get; private set; }
@ -20,59 +40,51 @@ public class Chapter : IComparable<Chapter>
public string ParentMangaId { get; internal set; } public string ParentMangaId { get; internal set; }
public Manga? ParentManga { get; init; } public Manga? ParentManga { get; init; }
public Chapter(Manga parentManga, string url, ChapterNumber chapterNumber, int? volumeNumber = null, string? title = null) public int CompareTo(Chapter? other)
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
{ {
this.ParentManga = parentManga; 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 Chapter(string parentMangaId, string url, ChapterNumber chapterNumber, public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber)
int? volumeNumber = null, string? title = null)
{ {
this.ParentMangaId = parentMangaId; ChapterNumber = chapterNumber;
this.Url = url;
this.ChapterNumber = chapterNumber;
this.VolumeNumber = volumeNumber;
this.Title = title;
this.ArchiveFileName = BuildArchiveFileName();
}
public MoveFileOrFolderJob? UpdateChapterNumber(ChapterNumber chapterNumber)
{
this.ChapterNumber = chapterNumber;
return UpdateArchiveFileName(); return UpdateArchiveFileName();
} }
public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber) public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber)
{ {
this.VolumeNumber = volumeNumber; VolumeNumber = volumeNumber;
return UpdateArchiveFileName(); return UpdateArchiveFileName();
} }
public MoveFileOrFolderJob? UpdateTitle(string? title) public MoveFileOrFolderJob? UpdateTitle(string? title)
{ {
this.Title = title; Title = title;
return UpdateArchiveFileName(); return UpdateArchiveFileName();
} }
private string BuildArchiveFileName() 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() private MoveFileOrFolderJob? UpdateArchiveFileName()
{ {
string oldPath = GetArchiveFilePath(); string oldPath = GetArchiveFilePath();
this.ArchiveFileName = BuildArchiveFileName(); ArchiveFileName = BuildArchiveFileName();
if (Downloaded) return Downloaded ? new MoveFileOrFolderJob(oldPath, GetArchiveFilePath()) : null;
{
return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath());
}
return null;
} }
/// <summary> /// <summary>
/// Creates full file path of chapter-archive /// Creates full file path of chapter-archive
/// </summary> /// </summary>
/// <returns>Filepath</returns> /// <returns>Filepath</returns>
internal string GetArchiveFilePath() internal string GetArchiveFilePath()
@ -86,27 +98,38 @@ public class Chapter : IComparable<Chapter>
return File.Exists(path); return File.Exists(path);
} }
public int CompareTo(Chapter? other) private static int CompareChapterNumbers(string ch1, string ch2)
{ {
if(other is not { } otherChapter) int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
throw new ArgumentException($"{other} can not be compared to {this}"); int[] ch2Arr = ch2.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
return this.VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
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, if (ch1Arr[i] < ch2Arr[j])
>0 => 1, return -1;
_ => this.ChapterNumber.CompareTo(otherChapter.ChapterNumber) if (ch1Arr[i] > ch2Arr[j])
}; return 1;
i++;
j++;
}
return 0;
} }
internal string GetComicInfoXmlString() 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("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))),
new XElement("LanguageISO", ParentManga.OriginalLanguage), 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("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))),
new XElement("Volume", this.VolumeNumber), new XElement("Volume", VolumeNumber),
new XElement("Number", this.ChapterNumber) new XElement("Number", ChapterNumber)
); );
return comicInfo.ToString(); return comicInfo.ToString();
} }

View File

@ -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<ChapterNumber>
{
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<char> destination, out int charsWritten, ReadOnlySpan<char> 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<char> s, IFormatProvider? provider) => Parse(s.ToString(), provider);
public static bool TryParse(ReadOnlySpan<char> 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<char> 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>(TOther value, out ChapterNumber result) where TOther : INumberBase<TOther>
{
throw new NotImplementedException();
}
public static bool TryConvertFromSaturating<TOther>(TOther value, out ChapterNumber result) where TOther : INumberBase<TOther>
{
throw new NotImplementedException();
}
public static bool TryConvertFromTruncating<TOther>(TOther value, out ChapterNumber result) where TOther : INumberBase<TOther>
{
throw new NotImplementedException();
}
public static bool TryConvertToChecked<TOther>(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
{
throw new NotImplementedException();
}
public static bool TryConvertToSaturating<TOther>(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
{
throw new NotImplementedException();
}
public static bool TryConvertToTruncating<TOther>(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
{
throw new NotImplementedException();
}
public static bool TryParse(ReadOnlySpan<char> 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);
}

View File

@ -12,7 +12,7 @@ using static System.IO.UnixFileMode;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection<string>? 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)] [MaxLength(64)]
public string ChapterId { get; init; } = chapterId; public string ChapterId { get; init; } = chapterId;
@ -79,7 +79,6 @@ public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null,
{ {
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
return; return;
DateTime start = DateTime.Now;
using Image image = Image.Load(imagePath); using Image image = Image.Load(imagePath);
File.Delete(imagePath); File.Delete(imagePath);
if(TrangaSettings.bwImages) if(TrangaSettings.bwImages)

View File

@ -4,7 +4,7 @@ using API.Schema.MangaConnectors;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? 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)] [MaxLength(64)]
public string MangaId { get; init; } = mangaId; public string MangaId { get; init; } = mangaId;
@ -12,11 +12,22 @@ public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string?
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Manga m = Manga ?? context.Manga.Find(MangaId)!; /*
MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!; * For some reason, directly using Manga from above instead of finding it again causes DBContext to consider
Chapter[] newChapters = connector.GetNewChapters(m); * 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.Chapters.AddRangeAsync(newChapters).Wait();
context.SaveChangesAsync().Wait(); context.SaveChangesAsync().Wait();
return newChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
return allNewChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
} }
} }

View File

@ -12,7 +12,7 @@ using static System.IO.UnixFileMode;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? 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)] [MaxLength(64)]
public string ChapterId { get; init; } = chapterId; public string ChapterId { get; init; } = chapterId;
@ -81,7 +81,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
{ {
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
return; return;
DateTime start = DateTime.Now; DateTime start = DateTime.UtcNow;
using Image image = Image.Load(imagePath); using Image image = Image.Load(imagePath);
File.Delete(imagePath); File.Delete(imagePath);
if(TrangaSettings.bwImages) if(TrangaSettings.bwImages)

View File

@ -43,8 +43,11 @@ public abstract class Job
RecurrenceMs = recurrenceMs; RecurrenceMs = recurrenceMs;
} }
public IEnumerable<Job> Run(PgsqlContext context) public IEnumerable<Job> Run(IServiceProvider serviceProvider)
{ {
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
this.state = JobState.Running; this.state = JobState.Running;
IEnumerable<Job> newJobs = RunInternal(context); IEnumerable<Job> newJobs = RunInternal(context);
this.state = JobState.Completed; this.state = JobState.Completed;

View File

@ -1,7 +1,7 @@
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? 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 FromLocation { get; init; } = fromLocation;
public string ToLocation { get; init; } = toLocation; public string ToLocation { get; init; } = toLocation;

View File

@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? 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)] [MaxLength(64)]
public string MangaId { get; init; } = mangaId; public string MangaId { get; init; } = mangaId;

View File

@ -6,7 +6,7 @@ namespace API.Schema.LibraryConnectors;
public class Kavita : LibraryConnector 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)
{ {
} }

View File

@ -5,7 +5,7 @@ namespace API.Schema.LibraryConnectors;
public class Komga : LibraryConnector 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) baseUrl, auth)
{ {
} }

View File

@ -7,7 +7,7 @@ namespace API.Schema;
public class Link(string linkProvider, string linkUrl) public class Link(string linkProvider, string linkUrl)
{ {
[MaxLength(64)] [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 LinkProvider { get; init; } = linkProvider;
public string LinkUrl { get; init; } = linkUrl; public string LinkUrl { get; init; } = linkUrl;

View File

@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -14,7 +14,7 @@ namespace API.Schema;
public class Manga public class Manga
{ {
[MaxLength(64)] [MaxLength(64)]
public string MangaId { get; init; } = TokenGen.CreateToken(typeof(Manga), 64); public string MangaId { get; init; }
[MaxLength(64)] [MaxLength(64)]
public string ConnectorId { get; init; } public string ConnectorId { get; init; }
@ -30,6 +30,7 @@ public class Manga
public float IgnoreChapterBefore { get; internal set; } public float IgnoreChapterBefore { get; internal set; }
public string MangaConnectorId { get; private set; } public string MangaConnectorId { get; private set; }
public MangaConnector? MangaConnector { get; private set; } public MangaConnector? MangaConnector { get; private set; }
public ICollection<Author>? Authors { get; internal set; } public ICollection<Author>? Authors { get; internal set; }
@ -57,6 +58,7 @@ public class Manga
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
float ignoreChapterBefore, string mangaConnectorId) float ignoreChapterBefore, string mangaConnectorId)
{ {
MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, connectorId);
ConnectorId = connectorId; ConnectorId = connectorId;
Name = name; Name = name;
Description = description; Description = description;

View File

@ -8,7 +8,7 @@ namespace API.Schema;
public class MangaAltTitle(string language, string title) public class MangaAltTitle(string language, string title)
{ {
[MaxLength(64)] [MaxLength(64)]
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", 64); public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", language, title);
[MaxLength(8)] [MaxLength(8)]
public string Language { get; init; } = language; public string Language { get; init; } = language;
public string Title { get; set; } = title; public string Title { get; set; } = title;

View File

@ -152,9 +152,7 @@ public class AsuraToon : MangaConnector
string chapterUrl = chapterInfo.GetAttributeValue("href", ""); string chapterUrl = chapterInfo.GetAttributeValue("href", "");
Match match = infoRex.Match(chapterInfo.InnerText); Match match = infoRex.Match(chapterInfo.InnerText);
if(!ChapterNumber.CanParse(match.Groups[1].Value)) string chapterNumber = new(match.Groups[1].Value);
continue;
ChapterNumber chapterNumber = new(match.Groups[1].Value);
string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null; string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
string url = $"https://asuracomic.net/series/{chapterUrl}"; string url = $"https://asuracomic.net/series/{chapterUrl}";
try try

View File

@ -97,7 +97,7 @@ public class Bato : MangaConnector
if (!uint.TryParse( if (!uint.TryParse(
document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0], document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
out uint year)) out uint year))
year = (uint)DateTime.Now.Year; year = (uint)DateTime.UtcNow.Year;
string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..") string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
.ChildNodes[2].InnerText; .ChildNodes[2].InnerText;
@ -159,9 +159,7 @@ public class Bato : MangaConnector
Match match = numberRex.Match(chapterUrl); Match match = numberRex.Match(chapterUrl);
string id = match.Groups[1].Value; string id = match.Groups[1].Value;
int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null; int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null;
if(ChapterNumber.CanParse(match.Groups[3].Value)) string chapterNumber = new(match.Groups[3].Value);
continue;
ChapterNumber chapterNumber = new(match.Groups[3].Value);
string url = $"https://bato.to{chapterUrl}?load=2"; string url = $"https://bato.to{chapterUrl}?load=2";
try try
{ {

View File

@ -229,9 +229,7 @@ public class MangaDex : MangaConnector
? attributes["chapter"]!.GetValue<string>() ? attributes["chapter"]!.GetValue<string>()
: null; : null;
if(chapterNumStr is null || ChapterNumber.CanParse(chapterNumStr)) string chapterNumber = new(chapterNumStr);
continue;
ChapterNumber chapterNumber = new(chapterNumStr);
if (attributes.ContainsKey("pages") && attributes["pages"] is not null && if (attributes.ContainsKey("pages") && attributes["pages"] is not null &&

View File

@ -128,9 +128,7 @@ public class MangaHere : MangaConnector
Match rexMatch = chapterRex.Match(url); Match rexMatch = chapterRex.Match(url);
int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value); int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value);
if(!ChapterNumber.CanParse(rexMatch.Groups[2].Value)) string chapterNumber = new(rexMatch.Groups[2].Value);
continue;
ChapterNumber chapterNumber = new(rexMatch.Groups[2].Value);
string fullUrl = $"https://www.mangahere.cc{url}"; string fullUrl = $"https://www.mangahere.cc{url}";
try try

View File

@ -127,7 +127,7 @@ public class MangaKatana : MangaConnector
while (description.StartsWith('\n')) while (description.StartsWith('\n'))
description = description.Substring(1); 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")) string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt"))
.InnerText.Split('-')[^1]; .InnerText.Split('-')[^1];
@ -185,9 +185,8 @@ public class MangaKatana : MangaConnector
.GetAttributeValue("href", ""); .GetAttributeValue("href", "");
int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null; int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null;
if(!ChapterNumber.CanParse(chapterNumRex.Match(url).Groups[1].Value))
continue; string chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
ChapterNumber chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
string chapterName = chapterNameRex.Match(fullString).Groups[1].Value; string chapterName = chapterNameRex.Match(fullString).Groups[1].Value;
try try
{ {

View File

@ -151,9 +151,8 @@ public class MangaLife : MangaConnector
? int.Parse(rexMatch.Groups[3].Value) ? int.Parse(rexMatch.Groups[3].Value)
: null; : null;
if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value))
continue; string chapterNumber = new(rexMatch.Groups[1].Value);
ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value);
string fullUrl = $"https://manga4life.com{url}"; string fullUrl = $"https://manga4life.com{url}";
fullUrl = fullUrl.Replace(Regex.Match(url,"(-page-[0-9])").Value,""); fullUrl = fullUrl.Replace(Regex.Match(url,"(-page-[0-9])").Value,"");
try try

View File

@ -181,9 +181,7 @@ public class Manganato : MangaConnector
int? volumeNumber = volRex.IsMatch(fullString) int? volumeNumber = volRex.IsMatch(fullString)
? int.Parse(volRex.Match(fullString).Groups[1].Value) ? int.Parse(volRex.Match(fullString).Groups[1].Value)
: null; : null;
if(!ChapterNumber.CanParse(chapterRex.Match(url).Groups[1].Value)) string chapterNumber = new(chapterRex.Match(url).Groups[1].Value);
continue;
ChapterNumber chapterNumber = new(chapterRex.Match(url).Groups[1].Value);
string chapterName = nameRex.Match(fullString).Groups[3].Value; string chapterName = nameRex.Match(fullString).Groups[3].Value;
try try
{ {

View File

@ -158,9 +158,8 @@ public class Mangaworld : MangaConnector
{ {
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value; string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
if(!ChapterNumber.CanParse(numberStr))
continue; string chapterNumber = new(numberStr);
ChapterNumber chapterNumber = new(numberStr);
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", ""); string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value; string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
try try
@ -178,9 +177,8 @@ public class Mangaworld : MangaConnector
foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter"))) 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; string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
if(!ChapterNumber.CanParse(numberStr))
continue; string chapterNumber = new(numberStr);
ChapterNumber chapterNumber = new(numberStr);
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", ""); string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value; string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
try try

View File

@ -148,9 +148,7 @@ public class ManhuaPlus : MangaConnector
{ {
Match rexMatch = urlRex.Match(url); Match rexMatch = urlRex.Match(url);
if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value)) string chapterNumber = new(rexMatch.Groups[1].Value);
continue;
ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value);
string fullUrl = url; string fullUrl = url;
try try
{ {

View File

@ -17,37 +17,38 @@ public class Weebcentral : MangaConnector
downloadClient = new ChromiumDownloadClient(); downloadClient = new ChromiumDownloadClient();
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "") public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(
string publicationTitle = "")
{ {
const int limit = 32; //How many values we want returned at once const int limit = 32; //How many values we want returned at once
var offset = 0; //"Page" int offset = 0; //"Page"
var requestUrl = string requestUrl =
$"{_baseUrl}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display"; $"{_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); downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
requestResult.htmlDocument == null) requestResult.htmlDocument == null)
{
return []; return [];
}
var publications = ParsePublicationsFromHtml(requestResult.htmlDocument); (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] publications =
ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications; return publications;
} }
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document) private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(
HtmlDocument document)
{ {
if (document.DocumentNode.SelectNodes("//article") == null) if (document.DocumentNode.SelectNodes("//article") == null)
return []; return [];
var urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']") List<string> urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']")
.Select(elem => elem.GetAttributeValue("href", "")).ToList(); .Select(elem => elem.GetAttributeValue("href", "")).ToList();
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new(); List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (var url in urls) foreach (string url in urls)
{ {
var manga = GetMangaFromUrl(url); (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? manga = GetMangaFromUrl(url);
if (manga is { } x) if (manga is { } x)
ret.Add(x); ret.Add(x);
} }
@ -55,30 +56,32 @@ public class Weebcentral : MangaConnector
return ret.ToArray(); return ret.ToArray();
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url) public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)?
GetMangaFromUrl(string url)
{ {
Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)"); 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 && if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 &&
requestResult.htmlDocument is not null) requestResult.htmlDocument is not null)
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url); return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
return null; return null;
} }
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(
HtmlDocument document, string publicationId, string websiteUrl)
{ {
var posterNode = HtmlNode? posterNode =
document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img"); 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"); HtmlNode? titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
var sortName = titleNode?.InnerText ?? "Undefined"; string sortName = titleNode?.InnerText ?? "Undefined";
HtmlNode[] authorsNodes = HtmlNode[] authorsNodes =
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span")?.ToArray() ?? []; document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span")?.ToArray() ?? [];
var authorNames = authorsNodes.Select(n => n.InnerText).ToList(); List<string> authorNames = authorsNodes.Select(n => n.InnerText).ToList();
List<Author> authors = authorNames.Select(n => new Author(n)).ToList(); List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
HtmlNode[] genreNodes = HtmlNode[] genreNodes =
@ -86,9 +89,9 @@ public class Weebcentral : MangaConnector
HashSet<string> tags = genreNodes.Select(n => n.InnerText).ToHashSet(); HashSet<string> tags = genreNodes.Select(n => n.InnerText).ToHashSet();
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList(); List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
var statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a"); HtmlNode? statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
var status = statusNode?.InnerText ?? ""; string status = statusNode?.InnerText ?? "";
var releaseStatus = MangaReleaseStatus.Unreleased; MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
switch (status.ToLower()) switch (status.ToLower())
{ {
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break; case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
@ -97,22 +100,22 @@ public class Weebcentral : MangaConnector
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break; case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
} }
var yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span"); HtmlNode? yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
var year = uint.Parse(yearNode?.InnerText ?? "0"); uint year = uint.Parse(yearNode?.InnerText ?? "0");
var descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p"); HtmlNode? descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
var description = descriptionNode?.InnerText ?? "Undefined"; string description = descriptionNode?.InnerText ?? "Undefined";
HtmlNode[] altTitleNodes = document.DocumentNode HtmlNode[] altTitleNodes = document.DocumentNode
.SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? []; .SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
Dictionary<string, string> altTitlesDict = new(), links = new(); Dictionary<string, string> 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); altTitlesDict.Add(i.ToString(), altTitleNodes[i].InnerText);
List<MangaAltTitle> altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList(); List<MangaAltTitle> 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, originalLanguage, releaseStatus, -1,
this, this,
authors, authors,
@ -123,7 +126,8 @@ public class Weebcentral : MangaConnector
return (manga, authors, mangaTags, [], altTitles); return (manga, authors, mangaTags, [], altTitles);
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId) public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(
string publicationId)
{ {
return GetMangaFromUrl($"https://weebcentral.com/series/{publicationId}"); return GetMangaFromUrl($"https://weebcentral.com/series/{publicationId}");
} }
@ -136,68 +140,68 @@ public class Weebcentral : MangaConnector
private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults) private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults)
{ {
Dictionary<SearchResult, int> similarity = new(); Dictionary<SearchResult, int> similarity = new();
foreach (var sr in unfilteredSearchResults) foreach (SearchResult sr in unfilteredSearchResults)
{ {
List<int> scores = new(); List<int> scores = new();
var filteredPublicationString = ToFilteredString(publicationTitle); string filteredPublicationString = ToFilteredString(publicationTitle);
var filteredSString = ToFilteredString(sr.s); string filteredSString = ToFilteredString(sr.s);
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredSString, filteredPublicationString)); 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)); scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredAString, filteredPublicationString));
} }
similarity.Add(sr, scores.Sum() / scores.Count); similarity.Add(sr, scores.Sum() / scores.Count);
} }
var ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList(); List<SearchResult> ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList();
return ret.ToArray(); return ret.ToArray();
} }
public override Chapter[] GetChapters(Manga manga, string language = "en") public override Chapter[] GetChapters(Manga manga, string language = "en")
{ {
var requestUrl = $"{_baseUrl}/series/{manga.MangaId}/full-chapter-list"; string requestUrl = $"{_baseUrl}/series/{manga.ConnectorId}/full-chapter-list";
var requestResult = RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default); downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>(); return [];
//Return Chapters ordered by Chapter-Number //Return Chapters ordered by Chapter-Number
if (requestResult.htmlDocument is null) if (requestResult.htmlDocument is null)
return Array.Empty<Chapter>(); return [];
var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument); List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
return chapters.Order().ToArray(); return chapters.Order().ToArray();
} }
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document) private List<Chapter> 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*)"); Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
var ret = chaptersWrapper.Descendants("a").Select(elem => List<Chapter> 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://")) 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); Match idMatch = idRex.Match(url);
var id = idMatch.Success ? idMatch.Groups[1].Value : null; string? id = idMatch.Success ? idMatch.Groups[1].Value : null;
var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ?? string chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
"Undefined"; "Undefined";
var chapterNumberMatch = chapterRex.Match(chapterNode); Match chapterNumberMatch = chapterRex.Match(chapterNode);
if(!chapterNumberMatch.Success || !ChapterNumber.CanParse(chapterNumberMatch.Groups[1].Value)) if (!chapterNumberMatch.Success)
return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null); return new Chapter(manga, "undefined", "-1");
ChapterNumber chapterNumber = new(chapterNumberMatch.Groups[1].Value);
return new Chapter(manga, url, chapterNumber, null, null); string chapterNumber = chapterNumberMatch.Groups[1].Value;
}).Where(elem => elem.ChapterNumber < ChapterNumber.Zero && elem.Url != "undefined").ToList(); return new Chapter(manga, url, chapterNumber);
}).Where(elem => elem.ChapterNumber.CompareTo("-1") != 0 && elem.Url != "undefined").ToList();
ret.Reverse(); ret.Reverse();
return ret; return ret;
@ -205,17 +209,15 @@ public class Weebcentral : MangaConnector
internal override string[] GetChapterImageUrls(Chapter chapter) internal override string[] GetChapterImageUrls(Chapter chapter)
{ {
var requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default); RequestResult requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||requestResult.htmlDocument is null) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
{ requestResult.htmlDocument is null) return [];
return [];
}
var document = requestResult.htmlDocument; HtmlDocument? document = requestResult.htmlDocument;
var imageNodes = HtmlNode[] imageNodes =
document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.Url}/images']/img")?.ToArray() ?? []; 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; return urls;
} }

View File

@ -7,7 +7,7 @@ namespace API.Schema;
public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null) public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
{ {
[MaxLength(64)] [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; public NotificationUrgency Urgency { get; init; } = urgency;

View File

@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace API.Schema.NotificationConnectors; namespace API.Schema.NotificationConnectors;
public class Gotify(string endpoint, string appToken) 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 Endpoint { get; init; } = endpoint;
public string AppToken { get; init; } = appToken; public string AppToken { get; init; } = appToken;

View File

@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace API.Schema.NotificationConnectors; namespace API.Schema.NotificationConnectors;
public class Lunasea(string id) 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 string Id { get; init; } = id;
public override void SendNotification(string title, string notificationText) public override void SendNotification(string title, string notificationText)

View File

@ -11,7 +11,7 @@ public class Ntfy : NotificationConnector
public string Auth { get; init; } public string Auth { get; init; }
public string Topic { 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; Endpoint = endpoint;
Auth = auth; Auth = auth;

View File

@ -5,36 +5,35 @@ namespace API;
public static class TokenGen public static class TokenGen
{ {
private const uint MinimumLength = 8; private const int MinimumLength = 32;
private const int MaximumLength = 64;
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789"; 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)
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;
}
public static string CreateTokenHash(string prefix, uint fullLength, string[] keys)
{ if (prefix.Length + 1 >= MaximumLength - MinimumLength)
if (prefix.Length + 1 >= fullLength - MinimumLength)
throw new ArgumentException("Prefix to long to create Token of meaningful length."); throw new ArgumentException("Prefix to long to create Token of meaningful length.");
int l = (int)(fullLength - prefix.Length - 1);
MD5 md5 = MD5.Create(); int tokenLength = MaximumLength - prefix.Length - 1;
byte[][] hashes = keys.Select(key => md5.ComputeHash(Encoding.UTF8.GetBytes(key))).ToArray();
byte[] xOrHash = new byte[l]; if (identifiers.Length == 0)
foreach (byte[] hash in hashes) {
for(int i = 0; i < hash.Length; i++) // No identifier, just create a random token
xOrHash[i] = (byte)(xOrHash[i] ^ (i >= hash.Length ? 0 : hash[i])); byte[] rng = new byte[tokenLength];
string key = new (xOrHash.Select(b => Chars[b % Chars.Length]).ToArray()); RandomNumberGenerator.Create().GetBytes(rng);
key = string.Join('-', prefix, key); string key = new(rng.Select(b => Chars[b % Chars.Length]).ToArray());
return key; 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);
} }
} }

View File

@ -43,7 +43,7 @@ public static class Tranga
if (notifications.Any()) if (notifications.Any())
{ {
DateTime max = notifications.MaxBy(n => n.Date)!.Date; 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) foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
{ {
@ -56,18 +56,22 @@ public static class Tranga
context.SaveChanges(); context.SaveChanges();
} }
private static void JobStarter(object? pgsqlContext) private static void JobStarter(object? serviceProviderObj)
{ {
if(pgsqlContext is null) return; if(serviceProviderObj is null) return;
PgsqlContext context = (PgsqlContext)pgsqlContext; IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null) return;
string TRANGA = "\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n"; string TRANGA =
"\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n";
Log.Info(TRANGA); Log.Info(TRANGA);
while (true) while (true)
{ {
List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList(); List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
foreach (Job job in completedJobs) foreach (Job job in completedJobs)
if(job.RecurrenceMs <= 0) if (job.RecurrenceMs <= 0)
context.Jobs.Remove(job); context.Jobs.Remove(job);
else else
{ {
@ -76,12 +80,16 @@ public static class Tranga
context.Jobs.Update(job); context.Jobs.Update(job);
} }
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList().Where(j => j.NextExecution < DateTime.UtcNow).ToList(); List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList()
.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
foreach (Job job in runJobs) 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<Job> newJobs = job.Run(context); IEnumerable<Job> newJobs = job.Run(serviceProvider);
context.Jobs.AddRange(newJobs); context.Jobs.AddRange(newJobs);
}); });
RunningJobs.Add(t, job); RunningJobs.Add(t, job);