mirror of
https://github.com/C9Glax/tranga.git
synced 2025-05-09 00:22:08 +02:00
Compare commits
12 Commits
7dc34c10e5
...
be6b3da1be
Author | SHA1 | Date | |
---|---|---|---|
be6b3da1be | |||
d0b775444d | |||
268441a47d | |||
b818f63f2a | |||
78a9322036 | |||
cc32b3dfae | |||
110a0bf481 | |||
fdbe585aa0 | |||
123a8b06b2 | |||
2350c5a04b | |||
f532e2ff76 | |||
6a8df2f5f8 |
@ -9,7 +9,6 @@ namespace API.MangaDownloadClients;
|
||||
internal class ChromiumDownloadClient : DownloadClient
|
||||
{
|
||||
private static IBrowser? _browser;
|
||||
private const int StartTimeoutMs = 10000;
|
||||
private readonly HttpDownloadClient _httpDownloadClient;
|
||||
|
||||
private static async Task<IBrowser> StartBrowser()
|
||||
@ -22,7 +21,7 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-setuid-sandbox",
|
||||
"--no-sandbox"},
|
||||
Timeout = StartTimeoutMs
|
||||
Timeout = 30000
|
||||
}, new LoggerFactory([new LogProvider()])); //TODO
|
||||
}
|
||||
|
||||
@ -67,15 +66,19 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
|
||||
private RequestResult MakeRequestBrowser(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
if (_browser is null)
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
IPage page = _browser.NewPageAsync().Result;
|
||||
page.DefaultTimeout = 10000;
|
||||
IResponse response;
|
||||
try
|
||||
{
|
||||
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
||||
//Log($"Page loaded. {url}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//Log($"Could not load Page {url}\n{e.Message}");
|
||||
page.CloseAsync();
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
693
API/Migrations/20250111180034_ChapterNumber.Designer.cs
generated
693
API/Migrations/20250111180034_ChapterNumber.Designer.cs
generated
@ -1,693 +0,0 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
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,10 +47,8 @@ namespace API.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
b.Property<float>("ChapterNumber")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
@ -66,8 +64,8 @@ namespace API.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
b.Property<float?>("VolumeNumber")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.HasKey("ChapterId");
|
||||
|
||||
|
@ -106,7 +106,6 @@ using (var scope = app.Services.CreateScope())
|
||||
new MangaKatana(),
|
||||
new MangaLife(),
|
||||
new Manganato(),
|
||||
new Mangasee(),
|
||||
new Mangaworld(),
|
||||
new ManhuaPlus(),
|
||||
new Weebcentral()
|
||||
|
@ -8,83 +8,71 @@ namespace API.Schema;
|
||||
[PrimaryKey("ChapterId")]
|
||||
public class Chapter : IComparable<Chapter>
|
||||
{
|
||||
public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
|
||||
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
|
||||
{
|
||||
ParentManga = parentManga;
|
||||
ArchiveFileName = BuildArchiveFileName();
|
||||
}
|
||||
|
||||
public Chapter(string parentMangaId, string url, string chapterNumber,
|
||||
int? volumeNumber = null, string? title = null)
|
||||
{
|
||||
ParentMangaId = parentMangaId;
|
||||
Url = url;
|
||||
ChapterNumber = chapterNumber;
|
||||
VolumeNumber = volumeNumber;
|
||||
Title = title;
|
||||
}
|
||||
|
||||
[MaxLength(64)] public string ChapterId { get; init; } = TokenGen.CreateToken(typeof(Chapter), 64);
|
||||
|
||||
[MaxLength(64)]
|
||||
public string ChapterId { get; init; } = TokenGen.CreateToken(typeof(Chapter), 64);
|
||||
public int? VolumeNumber { get; private set; }
|
||||
|
||||
[MaxLength(10)] public string ChapterNumber { get; private set; }
|
||||
|
||||
public ChapterNumber ChapterNumber { get; private set; }
|
||||
public string Url { get; internal set; }
|
||||
public string? Title { get; private set; }
|
||||
public string ArchiveFileName { get; private set; }
|
||||
public bool Downloaded { get; internal set; } = false;
|
||||
|
||||
|
||||
public string ParentMangaId { get; internal set; }
|
||||
public Manga? ParentManga { get; init; }
|
||||
|
||||
public int CompareTo(Chapter? other)
|
||||
public Chapter(Manga parentManga, string url, ChapterNumber chapterNumber, int? volumeNumber = null, string? title = null)
|
||||
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
|
||||
{
|
||||
if (other is not { } otherChapter)
|
||||
throw new ArgumentException($"{other} can not be compared to {this}");
|
||||
return VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
|
||||
{
|
||||
< 0 => -1,
|
||||
> 0 => 1,
|
||||
_ => CompareChapterNumbers(ChapterNumber, otherChapter.ChapterNumber)
|
||||
};
|
||||
this.ParentManga = parentManga;
|
||||
}
|
||||
|
||||
public Chapter(string parentMangaId, string url, ChapterNumber chapterNumber,
|
||||
int? volumeNumber = null, string? title = null)
|
||||
{
|
||||
this.ParentMangaId = parentMangaId;
|
||||
this.Url = url;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.Title = title;
|
||||
this.ArchiveFileName = BuildArchiveFileName();
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber)
|
||||
public MoveFileOrFolderJob? UpdateChapterNumber(ChapterNumber chapterNumber)
|
||||
{
|
||||
ChapterNumber = chapterNumber;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
return UpdateArchiveFileName();
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber)
|
||||
{
|
||||
VolumeNumber = volumeNumber;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
return UpdateArchiveFileName();
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob? UpdateTitle(string? title)
|
||||
{
|
||||
Title = title;
|
||||
this.Title = title;
|
||||
return UpdateArchiveFileName();
|
||||
}
|
||||
|
||||
private string BuildArchiveFileName()
|
||||
{
|
||||
return
|
||||
$"{ParentManga.Name} - Vol.{VolumeNumber ?? 0} Ch.{ChapterNumber}{(Title is null ? "" : $" - {Title}")}.cbz";
|
||||
return $"{this.ParentManga.Name} - Vol.{this.VolumeNumber ?? 0} Ch.{this.ChapterNumber}{(this.Title is null ? "" : $" - {this.Title}")}.cbz";
|
||||
}
|
||||
|
||||
private MoveFileOrFolderJob? UpdateArchiveFileName()
|
||||
{
|
||||
string oldPath = GetArchiveFilePath();
|
||||
ArchiveFileName = BuildArchiveFileName();
|
||||
if (Downloaded) return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath());
|
||||
this.ArchiveFileName = BuildArchiveFileName();
|
||||
if (Downloaded)
|
||||
{
|
||||
return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates full file path of chapter-archive
|
||||
/// Creates full file path of chapter-archive
|
||||
/// </summary>
|
||||
/// <returns>Filepath</returns>
|
||||
internal string GetArchiveFilePath()
|
||||
@ -98,35 +86,27 @@ public class Chapter : IComparable<Chapter>
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
private static int CompareChapterNumbers(string ch1, string ch2)
|
||||
public int CompareTo(Chapter? other)
|
||||
{
|
||||
int[] ch1Arr = ch1.Split('.').Select(c => int.Parse(c)).ToArray();
|
||||
int[] ch2Arr = ch2.Split('.').Select(c => int.Parse(c)).ToArray();
|
||||
|
||||
int i = 0, j = 0;
|
||||
|
||||
while (i < ch1Arr.Length && j < ch2Arr.Length)
|
||||
if(other is not { } otherChapter)
|
||||
throw new ArgumentException($"{other} can not be compared to {this}");
|
||||
return this.VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
|
||||
{
|
||||
if (ch1Arr[i] < ch2Arr[j])
|
||||
return -1;
|
||||
if (ch1Arr[i] > ch2Arr[j])
|
||||
return 1;
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
<0 => -1,
|
||||
>0 => 1,
|
||||
_ => this.ChapterNumber.CompareTo(otherChapter.ChapterNumber)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
internal string GetComicInfoXmlString()
|
||||
{
|
||||
XElement comicInfo = new("ComicInfo",
|
||||
XElement comicInfo = new XElement("ComicInfo",
|
||||
new XElement("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))),
|
||||
new XElement("LanguageISO", ParentManga.OriginalLanguage),
|
||||
new XElement("Title", Title),
|
||||
new XElement("Title", this.Title),
|
||||
new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))),
|
||||
new XElement("Volume", VolumeNumber),
|
||||
new XElement("Number", ChapterNumber)
|
||||
new XElement("Volume", this.VolumeNumber),
|
||||
new XElement("Number", this.ChapterNumber)
|
||||
);
|
||||
return comicInfo.ToString();
|
||||
}
|
||||
|
305
API/Schema/ChapterNumber.cs
Normal file
305
API/Schema/ChapterNumber.cs
Normal file
@ -0,0 +1,305 @@
|
||||
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);
|
||||
}
|
@ -152,7 +152,9 @@ public class AsuraToon : MangaConnector
|
||||
string chapterUrl = chapterInfo.GetAttributeValue("href", "");
|
||||
|
||||
Match match = infoRex.Match(chapterInfo.InnerText);
|
||||
string chapterNumber = new(match.Groups[1].Value);
|
||||
if(!ChapterNumber.CanParse(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 url = $"https://asuracomic.net/series/{chapterUrl}";
|
||||
try
|
||||
|
@ -159,7 +159,9 @@ public class Bato : MangaConnector
|
||||
Match match = numberRex.Match(chapterUrl);
|
||||
string id = match.Groups[1].Value;
|
||||
int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null;
|
||||
string chapterNumber = new(match.Groups[3].Value);
|
||||
if(ChapterNumber.CanParse(match.Groups[3].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(match.Groups[3].Value);
|
||||
string url = $"https://bato.to{chapterUrl}?load=2";
|
||||
try
|
||||
{
|
||||
|
@ -229,7 +229,9 @@ public class MangaDex : MangaConnector
|
||||
? attributes["chapter"]!.GetValue<string>()
|
||||
: null;
|
||||
|
||||
string chapterNumber = new(chapterNumStr);
|
||||
if(chapterNumStr is null || ChapterNumber.CanParse(chapterNumStr))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(chapterNumStr);
|
||||
|
||||
|
||||
if (attributes.ContainsKey("pages") && attributes["pages"] is not null &&
|
||||
|
@ -128,7 +128,9 @@ public class MangaHere : MangaConnector
|
||||
Match rexMatch = chapterRex.Match(url);
|
||||
|
||||
int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value);
|
||||
string chapterNumber = new(rexMatch.Groups[2].Value);
|
||||
if(!ChapterNumber.CanParse(rexMatch.Groups[2].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(rexMatch.Groups[2].Value);
|
||||
string fullUrl = $"https://www.mangahere.cc{url}";
|
||||
|
||||
try
|
||||
|
@ -185,8 +185,9 @@ public class MangaKatana : MangaConnector
|
||||
.GetAttributeValue("href", "");
|
||||
|
||||
int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null;
|
||||
|
||||
string chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
|
||||
if(!ChapterNumber.CanParse(chapterNumRex.Match(url).Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
|
||||
string chapterName = chapterNameRex.Match(fullString).Groups[1].Value;
|
||||
try
|
||||
{
|
||||
|
@ -151,8 +151,9 @@ public class MangaLife : MangaConnector
|
||||
? int.Parse(rexMatch.Groups[3].Value)
|
||||
: null;
|
||||
|
||||
|
||||
string chapterNumber = new(rexMatch.Groups[1].Value);
|
||||
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
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
@ -9,37 +10,33 @@ public class Manganato : MangaConnector
|
||||
{
|
||||
public Manganato() : base("Manganato", ["en"], ["manganato.com"])
|
||||
{
|
||||
downloadClient = new HttpDownloadClient();
|
||||
this.downloadClient = new HttpDownloadClient();
|
||||
}
|
||||
|
||||
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 = "")
|
||||
{
|
||||
var sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0))
|
||||
.ToLower();
|
||||
var requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
|
||||
var requestResult =
|
||||
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
|
||||
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, 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 [];
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications =
|
||||
ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||
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)
|
||||
{
|
||||
List<HtmlNode> searchResults = document.DocumentNode.Descendants("div")
|
||||
.Where(n => n.HasClass("search-story-item")).ToList();
|
||||
List<HtmlNode> searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("search-story-item")).ToList();
|
||||
List<string> urls = new();
|
||||
foreach (var mangaResult in searchResults)
|
||||
foreach (HtmlNode mangaResult in searchResults)
|
||||
{
|
||||
urls.Add(mangaResult.Descendants("a").First(n => n.HasClass("item-title")).GetAttributes()
|
||||
.First(a => a.Name == "href").Value);
|
||||
}
|
||||
|
||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
||||
foreach (var url in urls)
|
||||
foreach (string url in urls)
|
||||
{
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
|
||||
if (manga is { } x)
|
||||
@ -49,57 +46,54 @@ public class Manganato : MangaConnector
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
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://chapmanganato.com/{publicationId}");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var requestResult =
|
||||
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;
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Dictionary<string, string> altTitlesDict = new();
|
||||
Dictionary<string, string>? links = null;
|
||||
HashSet<string> tags = new();
|
||||
string[] authorNames = [];
|
||||
var originalLanguage = "";
|
||||
var releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
string originalLanguage = "";
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
var infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
|
||||
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
|
||||
|
||||
var sortName = infoNode.Descendants("h1").First().InnerText;
|
||||
string sortName = infoNode.Descendants("h1").First().InnerText;
|
||||
|
||||
var infoTable = infoNode.Descendants().First(d => d.Name == "table");
|
||||
|
||||
foreach (var row in infoTable.Descendants("tr"))
|
||||
HtmlNode infoTable = infoNode.Descendants().First(d => d.Name == "table");
|
||||
|
||||
foreach (HtmlNode row in infoTable.Descendants("tr"))
|
||||
{
|
||||
var key = row.SelectNodes("td").First().InnerText.ToLower();
|
||||
var value = row.SelectNodes("td").Last().InnerText;
|
||||
var keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||
string key = row.SelectNodes("td").First().InnerText.ToLower();
|
||||
string value = row.SelectNodes("td").Last().InnerText;
|
||||
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||
|
||||
switch (keySanitized)
|
||||
{
|
||||
case "alternative":
|
||||
string[] alts = value.Split(" ; ");
|
||||
for (var i = 0; i < alts.Length; i++)
|
||||
for(int i = 0; i < alts.Length; i++)
|
||||
altTitlesDict.Add(i.ToString(), alts[i]);
|
||||
break;
|
||||
case "authors":
|
||||
authorNames = value.Split('-');
|
||||
for (var i = 0; i < authorNames.Length; i++)
|
||||
for (int i = 0; i < authorNames.Length; i++)
|
||||
authorNames[i] = authorNames[i].Replace("\r\n", "");
|
||||
break;
|
||||
case "status":
|
||||
@ -108,62 +102,57 @@ public class Manganato : MangaConnector
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
}
|
||||
|
||||
break;
|
||||
case "genres":
|
||||
string[] genres = value.Split(" - ");
|
||||
for (var i = 0; i < genres.Length; i++)
|
||||
for (int i = 0; i < genres.Length; i++)
|
||||
genres[i] = genres[i].Replace("\r\n", "");
|
||||
tags = genres.ToHashSet();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
|
||||
List<MangaAltTitle> mangaAltTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList();
|
||||
|
||||
var coverUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img")
|
||||
.First()
|
||||
string coverUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First()
|
||||
.GetAttributes().First(a => a.Name == "src").Value;
|
||||
|
||||
var description = document.DocumentNode.Descendants("div")
|
||||
.First(d => d.HasClass("panel-story-info-description"))
|
||||
string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description"))
|
||||
.InnerText.Replace("Description :", "");
|
||||
while (description.StartsWith('\n'))
|
||||
description = description.Substring(1);
|
||||
|
||||
string pattern = "MMM dd,yyyy HH:mm";
|
||||
|
||||
var pattern = "MMM dd,yyyy HH:mm";
|
||||
|
||||
var oldestChapter = document.DocumentNode
|
||||
HtmlNode? oldestChapter = document.DocumentNode
|
||||
.SelectNodes("//span[contains(concat(' ',normalize-space(@class),' '),' chapter-time ')]").MaxBy(
|
||||
node => DateTime.ParseExact(node.GetAttributeValue("title", "Dec 31 2400, 23:59"), pattern,
|
||||
CultureInfo.InvariantCulture).Millisecond);
|
||||
|
||||
|
||||
var year = (uint)DateTime.ParseExact(
|
||||
oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59") ?? "Dec 31 2400, 23:59", pattern,
|
||||
uint year = (uint)DateTime.ParseExact(oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59")??"Dec 31 2400, 23:59", pattern,
|
||||
CultureInfo.InvariantCulture).Year;
|
||||
|
||||
Manga manga = new(publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
mangaAltTitles);
|
||||
|
||||
|
||||
return (manga, authors, mangaTags, [], mangaAltTitles);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
var requestUrl = $"https://chapmanganato.com/{manga.MangaId}";
|
||||
var requestResult =
|
||||
string requestUrl = $"https://chapmanganato.com/{manga.MangaId}";
|
||||
RequestResult 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 [];
|
||||
@ -176,25 +165,26 @@ public class Manganato : MangaConnector
|
||||
{
|
||||
List<Chapter> ret = new();
|
||||
|
||||
var chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
|
||||
HtmlNode chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
|
||||
|
||||
Regex volRex = new(@"Vol\.([0-9]+).*");
|
||||
Regex chapterRex = new(@"https:\/\/chapmanganato.[A-z]+\/manga-[A-z0-9]+\/chapter-([0-9\.]+)");
|
||||
Regex nameRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}:? (.*)");
|
||||
|
||||
foreach (var chapterInfo in chapterList.Descendants("li"))
|
||||
foreach (HtmlNode chapterInfo in chapterList.Descendants("li"))
|
||||
{
|
||||
var fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
|
||||
string fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
|
||||
|
||||
var url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
||||
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
||||
.GetAttributeValue("href", "");
|
||||
|
||||
|
||||
int? volumeNumber = volRex.IsMatch(fullString)
|
||||
? int.Parse(volRex.Match(fullString).Groups[1].Value)
|
||||
: null;
|
||||
|
||||
string chapterNumber = new(chapterRex.Match(url).Groups[1].Value);
|
||||
var chapterName = nameRex.Match(fullString).Groups[3].Value;
|
||||
if(!ChapterNumber.CanParse(chapterRex.Match(url).Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(chapterRex.Match(url).Groups[1].Value);
|
||||
string chapterName = nameRex.Match(fullString).Groups[3].Value;
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, chapterName));
|
||||
@ -203,19 +193,20 @@ public class Manganato : MangaConnector
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
ret.Reverse();
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
var requestUrl = chapter.Url;
|
||||
var requestResult =
|
||||
string requestUrl = chapter.Url;
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
|
||||
requestResult.htmlDocument is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
|
||||
return imageUrls;
|
||||
@ -225,9 +216,9 @@ public class Manganato : MangaConnector
|
||||
{
|
||||
List<string> ret = new();
|
||||
|
||||
var imageContainer =
|
||||
HtmlNode imageContainer =
|
||||
document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader"));
|
||||
foreach (var imageNode in imageContainer.Descendants("img"))
|
||||
foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
|
||||
ret.Add(imageNode.GetAttributeValue("src", ""));
|
||||
|
||||
return ret.ToArray();
|
||||
|
@ -1,214 +0,0 @@
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
using Newtonsoft.Json;
|
||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class Mangasee : MangaConnector
|
||||
{
|
||||
public Mangasee() : base("Mangasee", ["en"], ["mangasee123.com"])
|
||||
{
|
||||
this.downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
private struct SearchResult
|
||||
{
|
||||
public string i { get; set; }
|
||||
public string s { get; set; }
|
||||
public string[] a { get; set; }
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||
{
|
||||
string requestUrl = "https://mangasee123.com/_search.php";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SearchResult[] searchResults = JsonConvert.DeserializeObject<SearchResult[]>(requestResult.htmlDocument!.DocumentNode.InnerText) ??
|
||||
throw new NoNullAllowedException();
|
||||
SearchResult[] filteredResults = FilteredResults(publicationTitle, searchResults);
|
||||
|
||||
|
||||
string[] urls = filteredResults.Select(result => $"https://mangasee123.com/manga/{result.i}").ToArray();
|
||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> searchResultManga = new();
|
||||
foreach (string url in urls)
|
||||
{
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? newManga = GetMangaFromUrl(url);
|
||||
if(newManga is { } manga)
|
||||
searchResultManga.Add(manga);
|
||||
}
|
||||
return searchResultManga.ToArray();
|
||||
}
|
||||
catch (NoNullAllowedException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private readonly string[] _filterWords = {"a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni"};
|
||||
private string ToFilteredString(string input) => string.Join(' ', input.ToLower().Split(' ').Where(word => _filterWords.Contains(word) == false));
|
||||
private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults)
|
||||
{
|
||||
Dictionary<SearchResult, int> similarity = new();
|
||||
foreach (SearchResult sr in unfilteredSearchResults)
|
||||
{
|
||||
List<int> scores = new();
|
||||
string filteredPublicationString = ToFilteredString(publicationTitle);
|
||||
string filteredSString = ToFilteredString(sr.s);
|
||||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredSString, filteredPublicationString));
|
||||
foreach (string srA in sr.a)
|
||||
{
|
||||
string filteredAString = ToFilteredString(srA);
|
||||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredAString, filteredPublicationString));
|
||||
}
|
||||
similarity.Add(sr, scores.Sum() / scores.Count);
|
||||
}
|
||||
|
||||
List<SearchResult> ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList();
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://mangasee123.com/manga/{publicationId}");
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Regex publicationIdRex = new(@"https:\/\/mangasee123.com\/manga\/(.*)(\/.*)*");
|
||||
string publicationId = publicationIdRex.Match(url).Groups[1].Value;
|
||||
|
||||
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||
if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null)
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
|
||||
return null;
|
||||
}
|
||||
|
||||
private (Manga, List<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")
|
||||
{
|
||||
try
|
||||
{
|
||||
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{manga.MangaId}.xml");
|
||||
XElement[] chapterItems = doc.Descendants("item").ToArray();
|
||||
List<Chapter> chapters = new();
|
||||
Regex chVolRex = new(@".*chapter-([0-9\.]+)(?:-index-([0-9\.]+))?.*");
|
||||
foreach (XElement chapter in chapterItems)
|
||||
{
|
||||
string url = chapter.Descendants("link").First().Value;
|
||||
Match m = chVolRex.Match(url);
|
||||
int? volumeNumber = m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : null;
|
||||
|
||||
string chapterNumber = new(m.Groups[1].Value);
|
||||
|
||||
string chapterUrl = Regex.Replace(url, @"-page-[0-9]+(\.html)", ".html");
|
||||
try
|
||||
{
|
||||
chapters.Add(new Chapter(manga, chapterUrl,chapterNumber, volumeNumber, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -158,8 +158,9 @@ public class Mangaworld : MangaConnector
|
||||
{
|
||||
|
||||
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
|
||||
|
||||
string chapterNumber = new(numberStr);
|
||||
if(!ChapterNumber.CanParse(numberStr))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(numberStr);
|
||||
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
|
||||
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
|
||||
try
|
||||
@ -177,8 +178,9 @@ public class Mangaworld : MangaConnector
|
||||
foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter")))
|
||||
{
|
||||
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
|
||||
|
||||
string chapterNumber = new(numberStr);
|
||||
if(!ChapterNumber.CanParse(numberStr))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(numberStr);
|
||||
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
|
||||
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
|
||||
try
|
||||
|
@ -148,7 +148,9 @@ public class ManhuaPlus : MangaConnector
|
||||
{
|
||||
Match rexMatch = urlRex.Match(url);
|
||||
|
||||
string chapterNumber = new(rexMatch.Groups[1].Value);
|
||||
if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value);
|
||||
string fullUrl = url;
|
||||
try
|
||||
{
|
||||
|
@ -157,7 +157,7 @@ public class Weebcentral : MangaConnector
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
||||
{
|
||||
var requestUrl = $"{_baseUrl}/series/{manga.ConnectorId}/full-chapter-list";
|
||||
var requestUrl = $"{_baseUrl}/series/{manga.MangaId}/full-chapter-list";
|
||||
var requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
@ -182,7 +182,7 @@ public class Weebcentral : MangaConnector
|
||||
var url = elem.GetAttributeValue("href", "") ?? "Undefined";
|
||||
|
||||
if (!url.StartsWith("https://") && !url.StartsWith("http://"))
|
||||
return new Chapter(manga, "undefined", "-1", null, null);
|
||||
return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null);
|
||||
|
||||
var idMatch = idRex.Match(url);
|
||||
var id = idMatch.Success ? idMatch.Groups[1].Value : null;
|
||||
@ -192,13 +192,12 @@ public class Weebcentral : MangaConnector
|
||||
|
||||
var chapterNumberMatch = chapterRex.Match(chapterNode);
|
||||
|
||||
if(!chapterNumberMatch.Success)
|
||||
return new Chapter(manga, "undefined", "-1", null, null);
|
||||
if(!chapterNumberMatch.Success || !ChapterNumber.CanParse(chapterNumberMatch.Groups[1].Value))
|
||||
return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null);
|
||||
ChapterNumber chapterNumber = new(chapterNumberMatch.Groups[1].Value);
|
||||
|
||||
string chapterNumber = new(chapterNumberMatch.Groups[1].Value);
|
||||
var chapter = new Chapter(manga, url, chapterNumber, null, null);
|
||||
return chapter;
|
||||
}).Where(elem => elem.ChapterNumber.CompareTo("-1") != 0 && elem.Url != "undefined").ToList();
|
||||
return new Chapter(manga, url, chapterNumber, null, null);
|
||||
}).Where(elem => elem.ChapterNumber < ChapterNumber.Zero && elem.Url != "undefined").ToList();
|
||||
|
||||
ret.Reverse();
|
||||
return ret;
|
||||
|
@ -30,7 +30,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
|
||||
.HasValue<MangaKatana>("MangaKatana")
|
||||
.HasValue<MangaLife>("Manga4Life")
|
||||
.HasValue<Manganato>("Manganato")
|
||||
.HasValue<Mangasee>("Mangasee")
|
||||
.HasValue<Mangaworld>("Mangaworld")
|
||||
.HasValue<ManhuaPlus>("ManhuaPlus")
|
||||
.HasValue<Weebcentral>("Weebcentral")
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace API;
|
||||
|
||||
@ -20,4 +21,20 @@ public static class TokenGen
|
||||
key = string.Join('-', prefix, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static string CreateTokenHash(string prefix, uint fullLength, string[] keys)
|
||||
{
|
||||
if (prefix.Length + 1 >= fullLength - MinimumLength)
|
||||
throw new ArgumentException("Prefix to long to create Token of meaningful length.");
|
||||
int l = (int)(fullLength - prefix.Length - 1);
|
||||
MD5 md5 = MD5.Create();
|
||||
byte[][] hashes = keys.Select(key => md5.ComputeHash(Encoding.UTF8.GetBytes(key))).ToArray();
|
||||
byte[] xOrHash = new byte[l];
|
||||
foreach (byte[] hash in hashes)
|
||||
for(int i = 0; i < hash.Length; i++)
|
||||
xOrHash[i] = (byte)(xOrHash[i] ^ (i >= hash.Length ? 0 : hash[i]));
|
||||
string key = new (xOrHash.Select(b => Chars[b % Chars.Length]).ToArray());
|
||||
key = string.Join('-', prefix, key);
|
||||
return key;
|
||||
}
|
||||
}
|
@ -8,10 +8,10 @@ namespace API;
|
||||
|
||||
public static class Tranga
|
||||
{
|
||||
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
|
||||
public static Thread JobStarterThread { get; } = new (JobStarter);
|
||||
private static readonly Dictionary<Thread, Job> RunningJobs = new();
|
||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
||||
public static Thread NotificationSenderThread { get; } = new(NotificationSender);
|
||||
public static Thread JobStarterThread { get; } = new(JobStarter);
|
||||
|
||||
internal static void StartLogger()
|
||||
{
|
||||
@ -20,11 +20,10 @@ public static class Tranga
|
||||
|
||||
private static void NotificationSender(object? pgsqlContext)
|
||||
{
|
||||
if (pgsqlContext is null) return;
|
||||
if(pgsqlContext is null) return;
|
||||
PgsqlContext context = (PgsqlContext)pgsqlContext;
|
||||
|
||||
IQueryable<Notification> staleNotifications =
|
||||
context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
|
||||
IQueryable<Notification> staleNotifications = context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
|
||||
context.Notifications.RemoveRange(staleNotifications);
|
||||
context.SaveChanges();
|
||||
while (true)
|
||||
@ -32,7 +31,7 @@ public static class Tranga
|
||||
SendNotifications(context, NotificationUrgency.High);
|
||||
SendNotifications(context, NotificationUrgency.Normal);
|
||||
SendNotifications(context, NotificationUrgency.Low);
|
||||
|
||||
|
||||
context.SaveChanges();
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
@ -47,46 +46,40 @@ public static class Tranga
|
||||
if (DateTime.Now.Subtract(max) > TrangaSettings.NotificationUrgencyDelay(urgency))
|
||||
{
|
||||
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
|
||||
foreach (Notification notification in notifications)
|
||||
notificationConnector.SendNotification(notification.Title, notification.Message);
|
||||
{
|
||||
foreach (Notification notification in notifications)
|
||||
notificationConnector.SendNotification(notification.Title, notification.Message);
|
||||
}
|
||||
context.Notifications.RemoveRange(notifications);
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
private static void JobStarter(object? pgsqlContext)
|
||||
{
|
||||
if (pgsqlContext is null) return;
|
||||
if(pgsqlContext is null) return;
|
||||
PgsqlContext context = (PgsqlContext)pgsqlContext;
|
||||
|
||||
string TRANGA =
|
||||
"\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n";
|
||||
|
||||
string TRANGA = "\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n";
|
||||
Log.Info(TRANGA);
|
||||
while (true)
|
||||
{
|
||||
List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
|
||||
foreach (Job job in completedJobs)
|
||||
if (job.RecurrenceMs <= 0)
|
||||
{
|
||||
if(job.RecurrenceMs <= 0)
|
||||
context.Jobs.Remove(job);
|
||||
}
|
||||
else
|
||||
{
|
||||
job.LastExecution = DateTime.UtcNow;
|
||||
job.state = JobState.Waiting;
|
||||
context.Jobs.Update(job);
|
||||
}
|
||||
|
||||
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running).AsEnumerable()
|
||||
.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)
|
||||
{
|
||||
// If the job is already running, skip it
|
||||
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
|
||||
|
||||
Thread t = new(() =>
|
||||
Thread t = new (() =>
|
||||
{
|
||||
IEnumerable<Job> newJobs = job.Run(context);
|
||||
context.Jobs.AddRange(newJobs);
|
||||
@ -103,7 +96,7 @@ public static class Tranga
|
||||
RunningJobs.Remove(thread.thread);
|
||||
context.Jobs.Update(thread.job);
|
||||
}
|
||||
|
||||
|
||||
context.SaveChanges();
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user