From 6687ab4b3b30f611504a1aeaf049490bf70a92a0 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 8 Mar 2025 19:22:23 +0100 Subject: [PATCH] Port Manganato --- API/Program.cs | 3 +- API/Schema/Manga.cs | 2 +- .../Schema}/MangaConnectors/Manganato.cs | 157 ++++++++---------- README.md | 14 +- 4 files changed, 80 insertions(+), 96 deletions(-) rename {Tranga => API/Schema}/MangaConnectors/Manganato.cs (57%) diff --git a/API/Program.cs b/API/Program.cs index 7d1991b..0448685 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -114,7 +114,8 @@ using (var scope = app.Services.CreateScope()) new MangaKatana(), new Mangaworld(), new ManhuaPlus(), - new Weebcentral() + new Weebcentral(), + new Manganato() ]; MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); context.MangaConnectors.AddRange(newConnectors); diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs index f2e7f1d..ae4f345 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -119,7 +119,7 @@ public class Manga if (File.Exists(saveImagePath)) return saveImagePath; - RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover); + RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover, this.WebsiteUrl); if (coverResult.statusCode is < HttpStatusCode.Accepted or >= HttpStatusCode.Ambiguous) return SaveCoverImageToCache(--retries); diff --git a/Tranga/MangaConnectors/Manganato.cs b/API/Schema/MangaConnectors/Manganato.cs similarity index 57% rename from Tranga/MangaConnectors/Manganato.cs rename to API/Schema/MangaConnectors/Manganato.cs index 4a1d8c8..de5bb0b 100644 --- a/Tranga/MangaConnectors/Manganato.cs +++ b/API/Schema/MangaConnectors/Manganato.cs @@ -1,88 +1,94 @@ using System.Globalization; using System.Net; using System.Text.RegularExpressions; +using API.MangaDownloadClients; using HtmlAgilityPack; -using Tranga.Jobs; -namespace Tranga.MangaConnectors; +namespace API.Schema.MangaConnectors; public class Manganato : MangaConnector { - public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"]) + public Manganato() : base("Manganato", ["en"], + ["natomanga.com", "manganato.gg", "mangakakalot.gg", "nelomanga.com"], + "https://www.manganato.gg/images/favicon-manganato.webp") { - this.downloadClient = new HttpDownloadClient(clone); + this.downloadClient = new HttpDownloadClient(); } - public override Manga[] GetManga(string publicationTitle = "") + public override (Manga, List?, List?, List?, List?)[] GetManga( + string publicationTitle = "") { - Log($"Searching Publications. Term=\"{publicationTitle}\""); - string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower(); + string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)) + .ToLower(); string requestUrl = $"https://manganato.gg/search/story/{sanitizedTitle}"; RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.Default); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - return Array.Empty(); + return []; if (requestResult.htmlDocument is null) - return Array.Empty(); - Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument); - Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\""); + return []; + (Manga, List?, List?, List?, List?)[] publications = + ParsePublicationsFromHtml(requestResult.htmlDocument); return publications; } - private Manga[] ParsePublicationsFromHtml(HtmlDocument document) + private (Manga, List?, List?, List?, List?)[] ParsePublicationsFromHtml( + HtmlDocument document) { - List searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList(); - Log($"{searchResults.Count} items."); + List searchResults = + document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList(); List urls = new(); foreach (HtmlNode mangaResult in searchResults) { try { - urls.Add(mangaResult.Descendants("h3").First(n => n.HasClass("story_name")) - .Descendants("a").First().GetAttributeValue("href", "")); - } catch + urls.Add(mangaResult.Descendants("h3").First(n => n.HasClass("story_name")) + .Descendants("a").First().GetAttributeValue("href", "")); + } + catch { //failed to get a url, send it to the void } } - HashSet ret = new(); + List<(Manga, List?, List?, List?, List?)> ret = new(); foreach (string url in urls) { - Manga? manga = GetMangaFromUrl(url); - if (manga is not null) - ret.Add((Manga)manga); + (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); + if (manga is { } m) + ret.Add(m); } return ret.ToArray(); } - public override Manga? GetMangaFromId(string publicationId) + public override (Manga, List?, List?, List?, List?)? GetMangaFromId( + string publicationId) { return GetMangaFromUrl($"https://chapmanganato.com/{publicationId}"); } - public override Manga? GetMangaFromUrl(string url) + public override (Manga, List?, List?, List?, List?)? + 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; return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url); } - private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) + private (Manga, List?, List?, List?, List?) ParseSinglePublicationFromHtml( + HtmlDocument document, string publicationId, string websiteUrl) { Dictionary altTitles = new(); - Dictionary? links = null; - HashSet tags = new(); - string[] authors = Array.Empty(); - string originalLanguage = ""; - Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased; + List tags = new(); + List authors = new(); + MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased; HtmlNode infoNode = document.DocumentNode.Descendants("ul").First(d => d.HasClass("manga-info-text")); @@ -91,37 +97,36 @@ public class Manganato : MangaConnector foreach (HtmlNode li in infoNode.Descendants("li")) { string text = li.InnerText.Trim().ToLower(); - + if (text.StartsWith("author(s) :")) { - authors = li.Descendants("a").Select(a => a.InnerText.Trim()).ToArray(); + authors = li.Descendants("a").Select(a => a.InnerText.Trim()).Select(a => new Author(a)).ToList(); } else if (text.StartsWith("status :")) { string status = text.Replace("status :", "").Trim().ToLower(); if (string.IsNullOrWhiteSpace(status)) - releaseStatus = Manga.ReleaseStatusByte.Continuing; + releaseStatus = MangaReleaseStatus.Continuing; else if (status == "ongoing") - releaseStatus = Manga.ReleaseStatusByte.Continuing; + releaseStatus = MangaReleaseStatus.Continuing; else - releaseStatus = Enum.Parse(status, true); + releaseStatus = Enum.Parse(status, true); } else if (li.HasClass("genres")) { - tags = li.Descendants("a").Select(a => a.InnerText.Trim()).ToHashSet(); + tags = li.Descendants("a").Select(a => new MangaTag(a.InnerText.Trim())).ToList(); } } - string posterUrl = document.DocumentNode.Descendants("div").First(s => s.HasClass("manga-info-pic")).Descendants("img").First() + string posterUrl = document.DocumentNode.Descendants("div").First(s => s.HasClass("manga-info-pic")) + .Descendants("img").First() .GetAttributes().First(a => a.Name == "src").Value; - string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover, "https://www.manganato.gg/"); - string description = document.DocumentNode.SelectSingleNode("//div[@id='contentBox']") .InnerText.Replace("Description :", ""); while (description.StartsWith('\n')) description = description.Substring(1); - + string pattern = "MMM-dd-yyyy HH:mm"; HtmlNode? oldestChapter = document.DocumentNode @@ -130,32 +135,44 @@ public class Manganato : MangaConnector CultureInfo.InvariantCulture).Millisecond); - int year = DateTime.ParseExact(oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59")??"Dec 31 2400, 23:59", pattern, - CultureInfo.InvariantCulture).Year; - - Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, - year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); - AddMangaToCache(manga); - return manga; + uint year = Convert.ToUInt32(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, posterUrl, null, year, null, releaseStatus, + -1, this, authors, tags, [], []); + return (manga, authors, tags, [], []); } - public override Chapter[] GetChapters(Manga manga, string language="en") + public override Chapter[] GetChapters(Manga manga, string language = "en") { - Log($"Getting chapters {manga}"); - string requestUrl = manga.websiteUrl; + string requestUrl = manga.WebsiteUrl; RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.Default); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) return Array.Empty(); - + //Return Chapters ordered by Chapter-Number if (requestResult.htmlDocument is null) return Array.Empty(); List chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument); - Log($"Got {chapters.Count} chapters. {manga}"); return chapters.Order().ToArray(); } + internal override string[] GetChapterImageUrls(Chapter chapter) + { + 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; + } + private List ParseChaptersFromHtml(Manga manga, HtmlDocument document) { List ret = new(); @@ -177,54 +194,24 @@ public class Manganato : MangaConnector volumeNumber = "0"; try { - ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url)); + ret.Add(new Chapter(manga, url, chapterNumber, int.Parse(volumeNumber), chapterName)); } catch (Exception e) { - Log($"Failed to load chapter {chapterNumber}: {e.Message}"); } } + ret.Reverse(); return ret; } - public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null) - { - if (progressToken?.cancellationRequested ?? false) - { - progressToken.Cancel(); - return HttpStatusCode.RequestTimeout; - } - - Manga chapterParentManga = chapter.parentManga; - Log($"Retrieving chapter-info {chapter} {chapterParentManga}"); - string requestUrl = chapter.url; - RequestResult requestResult = - downloadClient.MakeRequest(requestUrl, RequestType.Default); - if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) - { - progressToken?.Cancel(); - return requestResult.statusCode; - } - - if (requestResult.htmlDocument is null) - { - progressToken?.Cancel(); - return HttpStatusCode.InternalServerError; - } - - string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument); - - return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, "https://www.manganato.gg", progressToken:progressToken); - } - private string[] ParseImageUrlsFromHtml(HtmlDocument document) { List ret = new(); HtmlNode imageContainer = document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader")); - foreach(HtmlNode imageNode in imageContainer.Descendants("img")) + foreach (HtmlNode imageNode in imageContainer.Descendants("img")) ret.Add(imageNode.GetAttributeValue("src", "")); return ret.ToArray(); diff --git a/README.md b/README.md index 5a5ea41..5fe709c 100644 --- a/README.md +++ b/README.md @@ -44,24 +44,24 @@ Tranga can download Chapters and Metadata from "Scanlation" sites such as - [MangaDex.org](https://mangadex.org/) (Multilingual) -- [Manganato.com](https://manganato.com/) (en) +- [Manganato.gg](https://manganato.com/) (en) - [MangaKatana.com](https://mangakatana.com) (en) - [Mangaworld.bz](https://www.mangaworld.bz/) (it) - [Bato.to](https://bato.to/v3x) (en) - [ManhuaPlus](https://manhuaplus.org/) (en) - [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.) - [Weebcentral](https://weebcentral.com) (en) -- [Webtoons](https://www.webtoons.com/en/) +- [Webtoons](https://www.webtoons.com/en/) (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+) and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). Notifications can be sent to your devices using [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/ -). +), or any other service that can use REST Webhooks. ### What this does and doesn't do Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga. -The configuration is all done through HTTP-Requests. [Documentation](docs/API_Calls_v2.md) +The configuration is all done through HTTP-Requests. _**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_ @@ -91,7 +91,6 @@ That is why I wanted to create my own project, in a language I understand, and t - [Html Agility Pack (HAP)](https://html-agility-pack.net/) - [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch) - [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license) -- [zstd-wrapper](https://github.com/oleg-st/ZstdSharp) [zstd](https://github.com/facebook/zstd) - 💙 Blåhaj 🦈

(back to top)

@@ -120,10 +119,7 @@ access the folder. ### Prerequisites -#### To Build -[.NET-Core 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) -#### To Run -[.NET-Core 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) scroll down a bit, should be on the right the second item. +.NET-9.0 See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues).