mirror of
https://github.com/C9Glax/tranga.git
synced 2025-05-09 00:22:08 +02:00
Compare commits
39 Commits
be6b3da1be
...
dccc9fdbef
Author | SHA1 | Date | |
---|---|---|---|
dccc9fdbef | |||
e6d40a7b36 | |||
a95cb90561 | |||
603e1b41d9 | |||
bb8a514830 | |||
9928abb674 | |||
ebb034e0c7 | |||
edacaaba8a | |||
d97da26994 | |||
8b923d73c4 | |||
1dca7ec569 | |||
7229fad6c5 | |||
814efd3528 | |||
2cd5d8bc4f | |||
5a864ab9b7 | |||
663e2e2ca0 | |||
![]() |
7f13d9b1e6 | ||
![]() |
0c9e3205c2 | ||
![]() |
6315940cd6 | ||
![]() |
ef7ebf022d | ||
![]() |
725813c2f3 | ||
![]() |
a69e12179b | ||
![]() |
45ca2695eb | ||
![]() |
bd9e79d026 | ||
![]() |
6bbd09072b | ||
![]() |
d6018b60ae | ||
4f7031ecfc | |||
![]() |
a713a006dd | ||
![]() |
ebe7e145aa | ||
![]() |
f7a285aabd | ||
786482398c | |||
![]() |
7921dcb1cb | ||
280d715a7c | |||
b4edcccafe | |||
1701881f4b | |||
e5be5703f8 | |||
ce217aae4f | |||
3abf7224d0 | |||
![]() |
b39dbd5671 |
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -69,6 +69,7 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
if (_browser is null)
|
if (_browser is null)
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||||
IPage page = _browser.NewPageAsync().Result;
|
IPage page = _browser.NewPageAsync().Result;
|
||||||
|
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
|
||||||
page.DefaultTimeout = 10000;
|
page.DefaultTimeout = 10000;
|
||||||
IResponse response;
|
IResponse response;
|
||||||
try
|
try
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
693
API/Migrations/20250111180034_ChapterNumber.Designer.cs
generated
Normal file
693
API/Migrations/20250111180034_ChapterNumber.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
API/Migrations/20250111180034_ChapterNumber.cs
Normal file
54
API/Migrations/20250111180034_ChapterNumber.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
|
||||||
|
@ -104,7 +104,6 @@ using (var scope = app.Services.CreateScope())
|
|||||||
new MangaDex(),
|
new MangaDex(),
|
||||||
new MangaHere(),
|
new MangaHere(),
|
||||||
new MangaKatana(),
|
new MangaKatana(),
|
||||||
new MangaLife(),
|
|
||||||
new Manganato(),
|
new Manganato(),
|
||||||
new Mangaworld(),
|
new Mangaworld(),
|
||||||
new ManhuaPlus(),
|
new ManhuaPlus(),
|
||||||
@ -124,7 +123,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");
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -8,71 +8,83 @@ 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; }
|
||||||
public bool Downloaded { get; internal set; } = false;
|
public bool Downloaded { get; internal set; } = false;
|
||||||
|
|
||||||
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
|
||||||
public Chapter(string parentMangaId, string url, ChapterNumber chapterNumber,
|
{
|
||||||
int? volumeNumber = null, string? title = null)
|
< 0 => -1,
|
||||||
{
|
> 0 => 1,
|
||||||
this.ParentMangaId = parentMangaId;
|
_ => CompareChapterNumbers(ChapterNumber, otherChapter.ChapterNumber)
|
||||||
this.Url = url;
|
};
|
||||||
this.ChapterNumber = chapterNumber;
|
|
||||||
this.VolumeNumber = volumeNumber;
|
|
||||||
this.Title = title;
|
|
||||||
this.ArchiveFileName = BuildArchiveFileName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoveFileOrFolderJob? UpdateChapterNumber(ChapterNumber chapterNumber)
|
public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber)
|
||||||
{
|
{
|
||||||
this.ChapterNumber = chapterNumber;
|
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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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 &&
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -1,188 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using HtmlAgilityPack;
|
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
|
||||||
|
|
||||||
public class MangaLife : MangaConnector
|
|
||||||
{
|
|
||||||
public MangaLife() : base("Manga4Life", ["en"], ["manga4life.com"])
|
|
||||||
{
|
|
||||||
this.downloadClient = new ChromiumDownloadClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
|
||||||
{
|
|
||||||
string sanitizedTitle = WebUtility.UrlEncode(publicationTitle);
|
|
||||||
string requestUrl = $"https://manga4life.com/search/?name={sanitizedTitle}";
|
|
||||||
RequestResult requestResult =
|
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
if (requestResult.htmlDocument is null)
|
|
||||||
return [];
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
|
||||||
return publications;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
|
||||||
{
|
|
||||||
return GetMangaFromUrl($"https://manga4life.com/manga/{publicationId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
|
||||||
{
|
|
||||||
Regex publicationIdRex = new(@"https:\/\/(www\.)?manga4life.com\/manga\/(.*)(\/.*)*");
|
|
||||||
string publicationId = publicationIdRex.Match(url).Groups[2].Value;
|
|
||||||
|
|
||||||
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
|
||||||
if(requestResult.htmlDocument is not null)
|
|
||||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
|
||||||
{
|
|
||||||
HtmlNode resultsNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']/div[last()]/div[1]/div");
|
|
||||||
if (resultsNode.Descendants("div").Count() == 1 && resultsNode.Descendants("div").First().HasClass("NoResults"))
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
|
||||||
|
|
||||||
foreach (HtmlNode resultNode in resultsNode.SelectNodes("div"))
|
|
||||||
{
|
|
||||||
string url = resultNode.Descendants().First(d => d.HasClass("SeriesName")).GetAttributeValue("href", "");
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl($"https://manga4life.com{url}");
|
|
||||||
if (manga is { } x)
|
|
||||||
ret.Add(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
|
||||||
{
|
|
||||||
string originalLanguage = "", status = "";
|
|
||||||
Dictionary<string, string> altTitles = new(), links = new();
|
|
||||||
HashSet<string> tags = new();
|
|
||||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
|
||||||
|
|
||||||
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
|
|
||||||
string coverUrl = posterNode.GetAttributeValue("src", "");
|
|
||||||
|
|
||||||
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
|
|
||||||
string sortName = titleNode.InnerText;
|
|
||||||
|
|
||||||
HtmlNode[] authorsNodes = document.DocumentNode
|
|
||||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Author(s):']/..").Descendants("a")
|
|
||||||
.ToArray();
|
|
||||||
List<string> authorNames = new();
|
|
||||||
foreach (HtmlNode authorNode in authorsNodes)
|
|
||||||
authorNames.Add(authorNode.InnerText);
|
|
||||||
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
|
|
||||||
|
|
||||||
HtmlNode[] genreNodes = document.DocumentNode
|
|
||||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Genre(s):']/..").Descendants("a")
|
|
||||||
.ToArray();
|
|
||||||
foreach (HtmlNode genreNode in genreNodes)
|
|
||||||
tags.Add(genreNode.InnerText);
|
|
||||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
|
||||||
|
|
||||||
HtmlNode yearNode = document.DocumentNode
|
|
||||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Released:']/..").Descendants("a")
|
|
||||||
.First();
|
|
||||||
uint year = uint.Parse(yearNode.InnerText);
|
|
||||||
|
|
||||||
HtmlNode[] statusNodes = document.DocumentNode
|
|
||||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Status:']/..").Descendants("a")
|
|
||||||
.ToArray();
|
|
||||||
foreach (HtmlNode statusNode in statusNodes)
|
|
||||||
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
status = statusNode.InnerText.Split(' ')[0];
|
|
||||||
switch (status.ToLower())
|
|
||||||
{
|
|
||||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
|
||||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
|
||||||
case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
|
||||||
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
|
|
||||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
HtmlNode descriptionNode = document.DocumentNode
|
|
||||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Description:']/..")
|
|
||||||
.Descendants("div").First();
|
|
||||||
string description = descriptionNode.InnerText;
|
|
||||||
|
|
||||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
|
||||||
originalLanguage, releaseStatus, -1,
|
|
||||||
this,
|
|
||||||
authors,
|
|
||||||
mangaTags,
|
|
||||||
[],
|
|
||||||
[]);
|
|
||||||
|
|
||||||
return (manga, authors, mangaTags, [], []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
|
||||||
{
|
|
||||||
RequestResult result = downloadClient.MakeRequest($"https://manga4life.com/manga/{manga.MangaId}", RequestType.Default, clickButton:"[class*='ShowAllChapters']");
|
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
|
||||||
{
|
|
||||||
return Array.Empty<Chapter>();
|
|
||||||
}
|
|
||||||
|
|
||||||
HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes(
|
|
||||||
"//a[contains(concat(' ',normalize-space(@class),' '),' ChapterLink ')]");
|
|
||||||
string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
|
|
||||||
Regex urlRex = new (@"-chapter-([0-9\\.]+)(-index-([0-9\\.]+))?");
|
|
||||||
|
|
||||||
List<Chapter> chapters = new();
|
|
||||||
foreach (string url in urls)
|
|
||||||
{
|
|
||||||
Match rexMatch = urlRex.Match(url);
|
|
||||||
|
|
||||||
int? volumeNumber = rexMatch.Groups[3].Success && rexMatch.Groups[3].Value.Length > 0
|
|
||||||
? int.Parse(rexMatch.Groups[3].Value)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value))
|
|
||||||
continue;
|
|
||||||
ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value);
|
|
||||||
string fullUrl = $"https://manga4life.com{url}";
|
|
||||||
fullUrl = fullUrl.Replace(Regex.Match(url,"(-page-[0-9])").Value,"");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
chapters.Add(new Chapter(manga, fullUrl, chapterNumber, volumeNumber, null));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
|
||||||
return chapters.Order().ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
|
||||||
{
|
|
||||||
RequestResult requestResult = this.downloadClient.MakeRequest(chapter.Url, RequestType.Default);
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
HtmlDocument document = requestResult.htmlDocument;
|
|
||||||
|
|
||||||
HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery"));
|
|
||||||
HtmlNode[] images = gallery.Descendants("img").Where(img => img.HasClass("img-fluid")).ToArray();
|
|
||||||
List<string> urls = new();
|
|
||||||
foreach(HtmlNode galleryImage in images)
|
|
||||||
urls.Add(galleryImage.GetAttributeValue("src", ""));
|
|
||||||
return urls.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
257
API/Schema/MangaConnectors/Webtoons.cs
Normal file
257
API/Schema/MangaConnectors/Webtoons.cs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using API.MangaDownloadClients;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
|
namespace API.Schema.MangaConnectors;
|
||||||
|
|
||||||
|
public class Webtoons : MangaConnector
|
||||||
|
{
|
||||||
|
|
||||||
|
public Webtoons() : base("Webtoons", ["en"], ["https://www.webtoons.com"])
|
||||||
|
{
|
||||||
|
this.downloadClient = new HttpDownloadClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||||
|
{
|
||||||
|
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
||||||
|
string requestUrl = $"https://www.webtoons.com/en/search?keyword={sanitizedTitle}&searchType=WEBTOON";
|
||||||
|
RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] publications =
|
||||||
|
ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||||
|
return publications;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
public override (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? GetMangaFromId(string publicationId)
|
||||||
|
{
|
||||||
|
PublicationManager pb = new PublicationManager(publicationId);
|
||||||
|
return GetMangaFromUrl($"https://www.webtoons.com/en/{pb.Category}/{pb.Title}/list?title_no={pb.Id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
public override (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? GetMangaFromUrl(string url)
|
||||||
|
{
|
||||||
|
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Regex regex = new Regex(@".*webtoons\.com/en/(?<category>[^/]+)/(?<title>[^/]+)/list\?title_no=(?<id>\d+).*");
|
||||||
|
Match match = regex.Match(url);
|
||||||
|
|
||||||
|
if(match.Success) {
|
||||||
|
PublicationManager pm = new PublicationManager(match.Groups["title"].Value, match.Groups["category"].Value, match.Groups["id"].Value);
|
||||||
|
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, pm.getPublicationId(), url);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
private (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||||
|
{
|
||||||
|
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//ul[contains(@class, 'card_lst')]");
|
||||||
|
if (!mangaList.ChildNodes.Any(node => node.Name == "li")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> urls = document.DocumentNode
|
||||||
|
.SelectNodes("//ul[contains(@class, 'card_lst')]/li/a")
|
||||||
|
.Select(node => node.GetAttributeValue("href", "https://www.webtoons.com"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
List<(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)> ret = new();
|
||||||
|
foreach (string url in urls)
|
||||||
|
{
|
||||||
|
(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? manga = GetMangaFromUrl(url);
|
||||||
|
if(manga is { } m)
|
||||||
|
ret.Add(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string capitalizeString(string str = "") {
|
||||||
|
if(str.Length == 0) return "";
|
||||||
|
if(str.Length == 1) return str.ToUpper();
|
||||||
|
return char.ToUpper(str[0]) + str.Substring(1).ToLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
private (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||||
|
{
|
||||||
|
HtmlNode infoNode1 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[1]/div[1]");
|
||||||
|
HtmlNode infoNode2 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[2]/div[2]");
|
||||||
|
|
||||||
|
string sortName = infoNode1.SelectSingleNode(".//h1[contains(@class, 'subj')]").InnerText;
|
||||||
|
string description = infoNode2.SelectSingleNode(".//p[contains(@class, 'summary')]")
|
||||||
|
.InnerText.Trim();
|
||||||
|
|
||||||
|
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'detail_body') and contains(@class, 'banner')]");
|
||||||
|
|
||||||
|
Regex regex = new Regex(@"url\((?<url>.*?)\)");
|
||||||
|
Match match = regex.Match(posterNode.GetAttributeValue("style", ""));
|
||||||
|
|
||||||
|
string coverUrl = match.Groups["url"].Value;
|
||||||
|
|
||||||
|
string genre = infoNode1.SelectSingleNode(".//h2[contains(@class, 'genre')]")
|
||||||
|
.InnerText.Trim();
|
||||||
|
List<MangaTag> mangaTags = [new MangaTag(genre)];
|
||||||
|
|
||||||
|
List<HtmlNode> authorsNodes = infoNode1.SelectSingleNode(".//div[contains(@class, 'author_area')]").Descendants("a").ToList();
|
||||||
|
List<Author> authors = authorsNodes.Select(node => new Author(node.InnerText.Trim())).ToList();
|
||||||
|
|
||||||
|
string originalLanguage = "";
|
||||||
|
|
||||||
|
uint year = 0;
|
||||||
|
|
||||||
|
string status1 = infoNode2.SelectSingleNode(".//p").InnerText;
|
||||||
|
string status2 = infoNode2.SelectSingleNode(".//p/span").InnerText;
|
||||||
|
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||||
|
if(status2.Length == 0 || status1.ToLower() == "completed") {
|
||||||
|
releaseStatus = MangaReleaseStatus.Completed;
|
||||||
|
} else if(status2.ToLower() == "up") {
|
||||||
|
releaseStatus = MangaReleaseStatus.Continuing;
|
||||||
|
}
|
||||||
|
|
||||||
|
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||||
|
originalLanguage, releaseStatus, -1,
|
||||||
|
this,
|
||||||
|
authors,
|
||||||
|
mangaTags,
|
||||||
|
[],
|
||||||
|
[]);
|
||||||
|
|
||||||
|
return (manga, authors, mangaTags, [], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
||||||
|
{
|
||||||
|
PublicationManager pm = new(manga.MangaId);
|
||||||
|
string requestUrl = $"https://www.webtoons.com/en/{pm.Category}/{pm.Title}/list?title_no={pm.Id}";
|
||||||
|
// Leaving this in for verification if the page exists
|
||||||
|
RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return Array.Empty<Chapter>();
|
||||||
|
|
||||||
|
// Get number of pages
|
||||||
|
int pages = requestResult.htmlDocument.DocumentNode.SelectSingleNode("//div[contains(@class, 'paginate')]").ChildNodes.ToArray().Length;
|
||||||
|
List<Chapter> chapters = new List<Chapter>();
|
||||||
|
|
||||||
|
for(int page = 1; page <= pages; page++) {
|
||||||
|
string pageRequestUrl = $"{requestUrl}&page={page}";
|
||||||
|
|
||||||
|
chapters.AddRange(ParseChaptersFromHtml(manga, pageRequestUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters.Order().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
|
||||||
|
{
|
||||||
|
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||||
|
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||||
|
{
|
||||||
|
return new List<Chapter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Chapter> ret = new();
|
||||||
|
|
||||||
|
foreach (HtmlNode chapterInfo in result.htmlDocument.DocumentNode.SelectNodes("//ul/li[contains(@class, '_episodeItem')]"))
|
||||||
|
{
|
||||||
|
HtmlNode infoNode = chapterInfo.SelectSingleNode(".//a");
|
||||||
|
string url = infoNode.GetAttributeValue("href", "");
|
||||||
|
|
||||||
|
string id = chapterInfo.GetAttributeValue("id", "");
|
||||||
|
if(id == "") continue;
|
||||||
|
string chapterNumber = chapterInfo.GetAttributeValue("data-episode-no", "");
|
||||||
|
if(chapterNumber == "") continue;
|
||||||
|
string chapterName = infoNode.SelectSingleNode(".//span[contains(@class, 'subj')]/span").InnerText.Trim();
|
||||||
|
ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||||
|
{
|
||||||
|
string requestUrl = chapter.Url;
|
||||||
|
// Leaving this in to check if the page exists
|
||||||
|
RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
|
||||||
|
return imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] ParseImageUrlsFromHtml(string mangaUrl)
|
||||||
|
{
|
||||||
|
RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestResult.htmlDocument.DocumentNode
|
||||||
|
.SelectNodes("//*[@id='_imageList']/img")
|
||||||
|
.Select(node =>
|
||||||
|
node.GetAttributeValue("data-url", ""))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PublicationManager {
|
||||||
|
public PublicationManager(string title = "", string category = "", string id = "") {
|
||||||
|
this.Title = title;
|
||||||
|
this.Category = category;
|
||||||
|
this.Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicationManager(string publicationId) {
|
||||||
|
string[] parts = publicationId.Split("|");
|
||||||
|
if(parts.Length == 3) {
|
||||||
|
this.Title = parts[0];
|
||||||
|
this.Category = parts[1];
|
||||||
|
this.Id = parts[2];
|
||||||
|
} else {
|
||||||
|
this.Title = "";
|
||||||
|
this.Category = "";
|
||||||
|
this.Id = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string getPublicationId() {
|
||||||
|
return $"{this.Title}|{this.Category}|{this.Id}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
@ -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,33 +100,34 @@ 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,
|
||||||
mangaTags,
|
mangaTags,
|
||||||
[],
|
[],
|
||||||
altTitles);
|
altTitles);
|
||||||
|
|
||||||
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);
|
|
||||||
|
string chapterNumber = chapterNumberMatch.Groups[1].Value;
|
||||||
return new Chapter(manga, url, chapterNumber, null, null);
|
return new Chapter(manga, url, chapterNumber);
|
||||||
}).Where(elem => elem.ChapterNumber < ChapterNumber.Zero && elem.Url != "undefined").ToList();
|
}).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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -28,7 +28,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
|
|||||||
.HasValue<Bato>("Bato")
|
.HasValue<Bato>("Bato")
|
||||||
.HasValue<MangaHere>("MangaHere")
|
.HasValue<MangaHere>("MangaHere")
|
||||||
.HasValue<MangaKatana>("MangaKatana")
|
.HasValue<MangaKatana>("MangaKatana")
|
||||||
.HasValue<MangaLife>("Manga4Life")
|
|
||||||
.HasValue<Manganato>("Manganato")
|
.HasValue<Manganato>("Manganato")
|
||||||
.HasValue<Mangaworld>("Mangaworld")
|
.HasValue<Mangaworld>("Mangaworld")
|
||||||
.HasValue<ManhuaPlus>("ManhuaPlus")
|
.HasValue<ManhuaPlus>("ManhuaPlus")
|
||||||
|
@ -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)
|
|
||||||
|
|
||||||
|
if (prefix.Length + 1 >= MaximumLength - 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.");
|
||||||
long l = fullLength - prefix.Length - 1;
|
|
||||||
byte[] rng = new byte[l];
|
int tokenLength = MaximumLength - prefix.Length - 1;
|
||||||
RandomNumberGenerator.Create().GetBytes(rng);
|
|
||||||
string key = new (rng.Select(b => Chars[b % Chars.Length]).ToArray());
|
if (identifiers.Length == 0)
|
||||||
key = string.Join('-', prefix, key);
|
{
|
||||||
return key;
|
// 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)
|
// Identifier provided, create a token based on the identifier hashed
|
||||||
{
|
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(string.Join("", identifiers)));
|
||||||
if (prefix.Length + 1 >= fullLength - MinimumLength)
|
string token = Convert.ToHexStringLower(hash);
|
||||||
throw new ArgumentException("Prefix to long to create Token of meaningful length.");
|
|
||||||
int l = (int)(fullLength - prefix.Length - 1);
|
return string.Join('-', prefix, token);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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();
|
||||||
string TRANGA = "\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n";
|
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
||||||
|
if (context is null) return;
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@ -75,13 +79,17 @@ public static class Tranga
|
|||||||
job.state = JobState.Waiting;
|
job.state = JobState.Waiting;
|
||||||
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);
|
||||||
@ -96,7 +104,7 @@ public static class Tranga
|
|||||||
RunningJobs.Remove(thread.thread);
|
RunningJobs.Remove(thread.thread);
|
||||||
context.Jobs.Update(thread.job);
|
context.Jobs.Update(thread.job);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
Thread.Sleep(2000);
|
Thread.Sleep(2000);
|
||||||
}
|
}
|
||||||
|
@ -45,14 +45,13 @@ Tranga can download Chapters and Metadata from "Scanlation" sites such as
|
|||||||
|
|
||||||
- [MangaDex.org](https://mangadex.org/) (Multilingual)
|
- [MangaDex.org](https://mangadex.org/) (Multilingual)
|
||||||
- [Manganato.com](https://manganato.com/) (en)
|
- [Manganato.com](https://manganato.com/) (en)
|
||||||
- [Mangasee.com](https://mangasee123.com/) (en)
|
|
||||||
- [MangaKatana.com](https://mangakatana.com) (en)
|
- [MangaKatana.com](https://mangakatana.com) (en)
|
||||||
- [Mangaworld.bz](https://www.mangaworld.bz/) (it)
|
- [Mangaworld.bz](https://www.mangaworld.bz/) (it)
|
||||||
- [Bato.to](https://bato.to/v3x) (en)
|
- [Bato.to](https://bato.to/v3x) (en)
|
||||||
- [Manga4Life](https://manga4life.com) (en)
|
|
||||||
- [ManhuaPlus](https://manhuaplus.org/) (en)
|
- [ManhuaPlus](https://manhuaplus.org/) (en)
|
||||||
- [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.)
|
- [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.)
|
||||||
- [Weebcentral](https://weebcentral.com) (en)
|
- [Weebcentral](https://weebcentral.com) (en)
|
||||||
|
- [Webtoons](https://www.webtoons.com/en/)
|
||||||
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
|
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
|
||||||
|
|
||||||
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
|
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
|
||||||
|
209
Tranga/MangaConnectors/WeebCentral.cs
Normal file
209
Tranga/MangaConnectors/WeebCentral.cs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||||
|
using Tranga.Jobs;
|
||||||
|
|
||||||
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
public class Weebcentral : MangaConnector
|
||||||
|
{
|
||||||
|
private readonly string _baseUrl = "https://weebcentral.com";
|
||||||
|
|
||||||
|
private readonly string[] _filterWords =
|
||||||
|
{ "a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni" };
|
||||||
|
|
||||||
|
public Weebcentral(GlobalBase clone) : base(clone, "Weebcentral", ["en"])
|
||||||
|
{
|
||||||
|
downloadClient = new ChromiumDownloadClient(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
|
{
|
||||||
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
|
const int limit = 32; //How many values we want returned at once
|
||||||
|
var offset = 0; //"Page"
|
||||||
|
var requestUrl =
|
||||||
|
$"{_baseUrl}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display";
|
||||||
|
var requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
|
||||||
|
requestResult.htmlDocument == null)
|
||||||
|
{
|
||||||
|
Log($"Failed to retrieve search: {requestResult.statusCode}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||||
|
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
|
||||||
|
|
||||||
|
return publications;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||||
|
{
|
||||||
|
if (document.DocumentNode.SelectNodes("//article") == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']")
|
||||||
|
.Select(elem => elem.GetAttributeValue("href", "")).ToList();
|
||||||
|
|
||||||
|
HashSet<Manga> ret = new();
|
||||||
|
foreach (var url in urls)
|
||||||
|
{
|
||||||
|
var manga = GetMangaFromUrl(url);
|
||||||
|
if (manga is not null)
|
||||||
|
ret.Add((Manga)manga);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
|
{
|
||||||
|
Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)");
|
||||||
|
var publicationId = publicationIdRex.Match(url).Groups[1].Value;
|
||||||
|
|
||||||
|
var 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 ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||||
|
{
|
||||||
|
var posterNode =
|
||||||
|
document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img");
|
||||||
|
var posterUrl = posterNode?.GetAttributeValue("src", "") ?? "";
|
||||||
|
var coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
|
||||||
|
|
||||||
|
var titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
|
||||||
|
var sortName = titleNode?.InnerText ?? "Undefined";
|
||||||
|
|
||||||
|
HtmlNode[] authorsNodes =
|
||||||
|
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span")?.ToArray() ?? [];
|
||||||
|
var authors = authorsNodes.Select(n => n.InnerText).ToList();
|
||||||
|
|
||||||
|
HtmlNode[] genreNodes =
|
||||||
|
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Tags(s): ']/span")?.ToArray() ?? [];
|
||||||
|
HashSet<string> tags = genreNodes.Select(n => n.InnerText).ToHashSet();
|
||||||
|
|
||||||
|
var statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
|
||||||
|
var status = statusNode?.InnerText ?? "";
|
||||||
|
Log("unable to parse status");
|
||||||
|
var releaseStatus = Manga.ReleaseStatusByte.Unreleased;
|
||||||
|
switch (status.ToLower())
|
||||||
|
{
|
||||||
|
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
|
||||||
|
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
|
||||||
|
case "complete": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
|
||||||
|
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
|
||||||
|
var year = Convert.ToInt32(yearNode?.InnerText ?? "0");
|
||||||
|
|
||||||
|
var descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
|
||||||
|
var description = descriptionNode?.InnerText ?? "Undefined";
|
||||||
|
|
||||||
|
HtmlNode[] altTitleNodes = document.DocumentNode
|
||||||
|
.SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
|
||||||
|
Dictionary<string, string> altTitles = new(), links = new();
|
||||||
|
for (var i = 0; i < altTitleNodes.Length; i++)
|
||||||
|
altTitles.Add(i.ToString(), altTitleNodes[i].InnerText);
|
||||||
|
|
||||||
|
var originalLanguage = "";
|
||||||
|
|
||||||
|
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
|
||||||
|
coverFileNameInCache, links,
|
||||||
|
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
|
||||||
|
AddMangaToCache(manga);
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Manga? GetMangaFromId(string publicationId)
|
||||||
|
{
|
||||||
|
return GetMangaFromUrl($"https://weebcentral.com/series/{publicationId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
||||||
|
{
|
||||||
|
Log($"Getting chapters {manga}");
|
||||||
|
var requestUrl = $"{_baseUrl}/series/{manga.publicationId}/full-chapter-list";
|
||||||
|
var requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
//Return Chapters ordered by Chapter-Number
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
return [];
|
||||||
|
var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
|
||||||
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
|
return chapters.Order().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
|
||||||
|
{
|
||||||
|
var chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body");
|
||||||
|
|
||||||
|
Regex chapterRex = new(@"(\d+(?:\.\d+)*)");
|
||||||
|
Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
|
||||||
|
|
||||||
|
var ret = chaptersWrapper.Descendants("a").Select(elem =>
|
||||||
|
{
|
||||||
|
var url = elem.GetAttributeValue("href", "") ?? "Undefined";
|
||||||
|
|
||||||
|
if (!url.StartsWith("https://") && !url.StartsWith("http://"))
|
||||||
|
return new Chapter(manga, null, null, "-1", "undefined");
|
||||||
|
|
||||||
|
var idMatch = idRex.Match(url);
|
||||||
|
var id = idMatch.Success ? idMatch.Groups[1].Value : null;
|
||||||
|
|
||||||
|
var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
|
||||||
|
"Undefined";
|
||||||
|
|
||||||
|
var chapterNumberMatch = chapterRex.Match(chapterNode);
|
||||||
|
var chapterNumber = chapterNumberMatch.Success ? chapterNumberMatch.Groups[1].Value : "-1";
|
||||||
|
|
||||||
|
return new Chapter(manga, null, null, chapterNumber, url, id);
|
||||||
|
}).Where(elem => elem.chapterNumber != -1 && elem.url != "undefined").ToList();
|
||||||
|
|
||||||
|
ret.Reverse();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
|
{
|
||||||
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
|
{
|
||||||
|
progressToken.Cancel();
|
||||||
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chapterParentManga = chapter.parentManga;
|
||||||
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
|
{
|
||||||
|
progressToken.Cancel();
|
||||||
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
|
|
||||||
|
var requestResult = downloadClient.MakeRequest(chapter.url, RequestType.Default);
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
{
|
||||||
|
progressToken?.Cancel();
|
||||||
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = requestResult.htmlDocument;
|
||||||
|
|
||||||
|
var imageNodes =
|
||||||
|
document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.url}/images']/img")?.ToArray() ?? [];
|
||||||
|
var urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray();
|
||||||
|
|
||||||
|
return DownloadChapterImages(urls, chapter, RequestType.MangaImage, progressToken: progressToken);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user