From 6576d06bc975d655e5a3f99f3b71e184380f1e8b Mon Sep 17 00:00:00 2001 From: glax Date: Sat, 20 Sep 2025 21:55:42 +0200 Subject: [PATCH 01/13] Add MangaPark --- API/MangaConnectors/MangaConnector.cs | 2 - API/MangaConnectors/MangaPark.cs | 207 ++++++++++++++++++++++++ API/Schema/MangaContext/MangaContext.cs | 1 + API/Tranga.cs | 2 +- Tranga.sln.DotSettings | 5 + 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 API/MangaConnectors/MangaPark.cs diff --git a/API/MangaConnectors/MangaConnector.cs b/API/MangaConnectors/MangaConnector.cs index 184e740..c462856 100644 --- a/API/MangaConnectors/MangaConnector.cs +++ b/API/MangaConnectors/MangaConnector.cs @@ -16,9 +16,7 @@ namespace API.MangaConnectors; public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl) { [NotMapped] internal DownloadClient downloadClient { get; init; } = null!; - [NotMapped] protected ILog Log { get; init; } = LogManager.GetLogger(name); - [StringLength(32)] public string Name { get; init; } = name; [StringLength(8)] public string[] SupportedLanguages { get; init; } = supportedLanguages; [StringLength(2048)] public string IconUrl { get; init; } = iconUrl; diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs new file mode 100644 index 0000000..76fbb9c --- /dev/null +++ b/API/MangaConnectors/MangaPark.cs @@ -0,0 +1,207 @@ +using System.Net; +using System.Text.RegularExpressions; +using API.MangaDownloadClients; +using API.Schema.MangaContext; +using HtmlAgilityPack; + +namespace API.MangaConnectors; + +public class MangaPark : MangaConnector +{ + public MangaPark() : base("MangaPark", + ["en"], + ["mangapark.com", "mangapark.net", "mangapark.org", "mangapark.me", "mangapark.io", "mangapark.to", "comicpark.org", "comicpark.to", "readpark.org", "readpark.net", "parkmanga.com", "parkmanga.net", "parkmanga.org", "mpark.to"], + "https://mangapark.com/static-assets/img/favicon.ico") + { + this.downloadClient = new HttpDownloadClient(); + } + + public override (Manga, MangaConnectorId)[] SearchManga(string mangaSearchName) + { + foreach (string uri in BaseUris) + if (SearchMangaWithDomain(mangaSearchName, uri) is { } result) + return result; + return []; + } + + private (Manga, MangaConnectorId)[]? SearchMangaWithDomain(string mangaSearchName, string domain) + { + Uri baseUri = new ($"https://{domain}/"); + Uri search = new(baseUri, $"search?word={mangaSearchName}&lang={Tranga.Settings.DownloadLanguage}"); + + HtmlDocument document = new(); + List<(Manga, MangaConnectorId)> ret = []; + + for (int page = 1;; page++) // break; in loop + { + Uri pageSearch = new(search, $"&page={page}"); + if (downloadClient.MakeRequest(pageSearch.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) + { + document.Load(result.result); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullable types + if (document.DocumentNode.SelectSingleNode("//button[contains(text(),\"No Data\")]") is not null) // No results found + break; + + HtmlNode resultsListNode = document.GetNodeWith("jp_1"); + ret.AddRange(resultsListNode.ChildNodes.Select(n => ParseSingleMangaFromSearchResultsList(baseUri, n))); + }else + return null; + } + + return ret.ToArray(); + } + + private (Manga, MangaConnectorId) ParseSingleMangaFromSearchResultsList(Uri baseUri, HtmlNode resultNode) + { + HtmlNode titleAndLinkNode = resultNode.SelectSingleNode("//a[contains(@href,'title')]"); + string link = titleAndLinkNode.Attributes["href"].Value; + + return ((Manga, MangaConnectorId))GetMangaFromUrl(new Uri(baseUri, link).ToString())!; + } + + public override (Manga, MangaConnectorId)? GetMangaFromId(string mangaIdOnSite) + { + foreach (string uri in BaseUris) + if (GetMangaFromIdWithDomain(mangaIdOnSite, uri) is { } result) + return result; + return null; + } + + private (Manga, MangaConnectorId)? GetMangaFromIdWithDomain(string mangaIdOnSite, string domain) + { + Uri baseUri = new ($"https://{domain}/"); + return GetMangaFromUrl(new Uri(baseUri, $"title/{mangaIdOnSite}").ToString()); + } + + public override (Manga, MangaConnectorId)? GetMangaFromUrl(string url) + { + if (downloadClient.MakeRequest(url, RequestType.Default) is + { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) + { + HtmlDocument document = new(); + document.Load(result.result); + + string name = document.GetNodeWith("2x", "q:id").InnerText; + string description = document.GetNodeWith("0a_9").InnerText; + + string coverRelative = document.GetNodeWith("q1_1").GetAttributeValue("src", ""); + string coverUrl = $"{url.Substring(0, url.IndexOf('/', 9))}{coverRelative}"; + + MangaReleaseStatus releaseStatus = document.GetNodeWith("Yn_5").InnerText.ToLower() switch + { + "pending" => MangaReleaseStatus.Unreleased, + "ongoing" => MangaReleaseStatus.Continuing, + "completed" => MangaReleaseStatus.Completed, + "hiatus" => MangaReleaseStatus.OnHiatus, + "cancelled" => MangaReleaseStatus.Cancelled, + _ => MangaReleaseStatus.Unreleased + }; + + ICollection authors = document.GetNodeWith("tz_4") + .ChildNodes.Where(n => n.Name == "a") + .Select(n => n.InnerText) + .Select(t => new Author(t)).ToList(); + + ICollection mangaTags = document.GetNodesWith("kd_0") + .Select(n => n.InnerText) + .Select(t => new MangaTag(t)).ToList(); + + ICollection links = []; + + ICollection altTitles = document.GetNodeWith("tz_2") + .ChildNodes.Where(n => n.InnerText.Length > 1) + .Select(n => n.InnerText) + .Select(t => new AltTitle(string.Empty, t)).ToList(); + + Manga m = new (name, description, coverUrl, releaseStatus, authors, mangaTags, links, altTitles); + MangaConnectorId mcId = new(m, this, url.Split('/').Last(), url); + m.MangaConnectorIds.Add(mcId); + return (m, mcId); + } + else return null; + } + + public override (Chapter, MangaConnectorId)[] GetChapters(MangaConnectorId mangaId, string? language = null) + { + foreach (string uri in BaseUris) + if (GetChaptersFromDomain(mangaId, uri) is { } result) + return result; + return []; + } + + private (Chapter, MangaConnectorId)[]? GetChaptersFromDomain(MangaConnectorId mangaId, string domain) + { + Uri baseUri = new ($"https://{domain}/"); + Uri requestUri = new (baseUri, $"title/{mangaId.IdOnConnectorSite}"); + if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is + { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) + { + HtmlDocument document = new(); + document.Load(result.result); + + HtmlNodeCollection chapterNodes = document.GetNodesWith("8t_8"); + + return chapterNodes.Select(n => ParseChapter(mangaId.Obj, n, baseUri)).ToArray(); + } + else return null; + } + + private readonly Regex _volChTitleRex = new(@"(?:.*(?:Vol\.?(?:ume)?)\s*([0-9]+))?.*(?:Ch\.?(?:apter)?)\s*([0-9\.]+)(?::\s+(.*))?"); + private (Chapter, MangaConnectorId) ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri) + { + HtmlNode linkNode = chapterNode.SelectSingleNode("/div[1]/a"); + Match linkMatch = _volChTitleRex.Match(linkNode.InnerText); + HtmlNode? titleNode = chapterNode.SelectSingleNode("/div[1]/span"); + + if (!linkMatch.Success || !linkMatch.Groups[2].Success) + { + Log.Error($"Unable to parse Chapter: {chapterNode.InnerHtml}"); + throw new ($"Unable to parse Chapter: {chapterNode.InnerHtml}"); + } + + string chapterNumber = linkMatch.Groups[2].Value; + int? volumeNumber = linkMatch.Groups[1].Success ? int.Parse(linkMatch.Groups[1].Value) : null; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullables + string? title = titleNode is not null ? titleNode.InnerText[2..] : (linkMatch.Groups[3].Success ? linkMatch.Groups[3].Value : null); + + string url = new Uri(baseUri, linkNode.GetAttributeValue("href", "")).ToString(); + string id = linkNode.GetAttributeValue("href", "")[7..]; + + Chapter chapter = new (manga, chapterNumber, volumeNumber, title); + MangaConnectorId chId = new(chapter, this, id, url); + chapter.MangaConnectorIds.Add(chId); + + return (chapter, chId); + } + + internal override string[] GetChapterImageUrls(MangaConnectorId chapterId) + { + foreach (string uri in BaseUris) + if (GetChapterImageUrlsFromDomain(chapterId, uri) is { } result) + return result; + return []; + } + + private string[]? GetChapterImageUrlsFromDomain(MangaConnectorId chapterId, string domain) + { + Uri baseUri = new ($"https://{domain}/"); + Uri requestUri = new (baseUri, $"title/{chapterId.IdOnConnectorSite}"); + if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is + { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) + { + HtmlDocument document = new(); + document.Load(result.result); + + HtmlNodeCollection imageNodes = document.GetNodesWith("8X_2"); + + return imageNodes.Select(n => n.SelectSingleNode("/div/img").GetAttributeValue("src", "")).ToArray(); + } + else return null; + } +} + +internal static class Helper +{ + internal static HtmlNode GetNodeWith(this HtmlDocument document, string search, string selector = "q:key") => document.DocumentNode.SelectSingleNode($"//*[@${selector}=${search}]"); + internal static HtmlNodeCollection GetNodesWith(this HtmlDocument document, string search, string selector = "q:key") => document.DocumentNode.SelectNodes($"//*[@${selector}=${search}]"); +} \ No newline at end of file diff --git a/API/Schema/MangaContext/MangaContext.cs b/API/Schema/MangaContext/MangaContext.cs index 4d7069e..61f3cb8 100644 --- a/API/Schema/MangaContext/MangaContext.cs +++ b/API/Schema/MangaContext/MangaContext.cs @@ -23,6 +23,7 @@ public class MangaContext(DbContextOptions options) : TrangaBaseCo .HasDiscriminator(c => c.Name) .HasValue("Global") .HasValue("MangaDex") + .HasValue("MangaPark") .HasValue("Mangaworld"); //Manga has many Chapters diff --git a/API/Tranga.cs b/API/Tranga.cs index f9561c9..b948cf6 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -20,7 +20,7 @@ public static class Tranga private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga)); internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()]; - internal static readonly MangaConnector[] MangaConnectors = [new Global(), new MangaDex(), new Mangaworld()]; + internal static readonly MangaConnector[] MangaConnectors = [new Global(), new MangaDex(), new Mangaworld(), new MangaPark()]; internal static TrangaSettings Settings = TrangaSettings.Load(); // ReSharper disable MemberCanBePrivate.Global diff --git a/Tranga.sln.DotSettings b/Tranga.sln.DotSettings index f95497a..2af9571 100644 --- a/Tranga.sln.DotSettings +++ b/Tranga.sln.DotSettings @@ -2,6 +2,7 @@ True True True + True True True True @@ -11,9 +12,13 @@ True True True + True True True + True True + True + True True True True From 94c220fafc396a81fd0c021ec4c725d7d5c3a9e6 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 01:32:55 +0200 Subject: [PATCH 02/13] Fix issues with namespaces in xpath --- API/MangaConnectors/MangaDex.cs | 3 +- API/MangaConnectors/MangaPark.cs | 76 +++++++++++++++++++------------ API/MangaConnectors/Mangaworld.cs | 3 +- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/API/MangaConnectors/MangaDex.cs b/API/MangaConnectors/MangaDex.cs index 01e0d88..e6f4188 100644 --- a/API/MangaConnectors/MangaDex.cs +++ b/API/MangaConnectors/MangaDex.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using System.Web; using API.MangaDownloadClients; using API.Schema.MangaContext; using Newtonsoft.Json.Linq; @@ -29,7 +30,7 @@ public class MangaDex : MangaConnector while(offset < total) { string requestUrl = - $"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" + + $"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={HttpUtility.UrlEncode(mangaSearchName)}" + $"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" + $"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'"; offset += Limit; diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 76fbb9c..772e282 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.RegularExpressions; +using System.Web; using API.MangaDownloadClients; using API.Schema.MangaContext; using HtmlAgilityPack; @@ -26,24 +27,21 @@ public class MangaPark : MangaConnector private (Manga, MangaConnectorId)[]? SearchMangaWithDomain(string mangaSearchName, string domain) { - Uri baseUri = new ($"https://{domain}/"); - Uri search = new(baseUri, $"search?word={mangaSearchName}&lang={Tranga.Settings.DownloadLanguage}"); + Uri baseUri = new($"https://{domain}/"); - HtmlDocument document = new(); List<(Manga, MangaConnectorId)> ret = []; for (int page = 1;; page++) // break; in loop { - Uri pageSearch = new(search, $"&page={page}"); - if (downloadClient.MakeRequest(pageSearch.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) + Uri searchUri = new(baseUri, $"search?word={HttpUtility.UrlEncode(mangaSearchName)}&lang={Tranga.Settings.DownloadLanguage}&page={page}"); + if (downloadClient.MakeRequest(searchUri.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { - document.Load(result.result); + HtmlDocument document= result.CreateDocument(); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullable types if (document.DocumentNode.SelectSingleNode("//button[contains(text(),\"No Data\")]") is not null) // No results found break; - - HtmlNode resultsListNode = document.GetNodeWith("jp_1"); - ret.AddRange(resultsListNode.ChildNodes.Select(n => ParseSingleMangaFromSearchResultsList(baseUri, n))); + + ret.AddRange(document.GetNodesWith("q4_9").Select(n => ParseSingleMangaFromSearchResultsList(baseUri, n))); }else return null; } @@ -78,16 +76,23 @@ public class MangaPark : MangaConnector if (downloadClient.MakeRequest(url, RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { - HtmlDocument document = new(); - document.Load(result.result); + HtmlDocument document= result.CreateDocument(); - string name = document.GetNodeWith("2x", "q:id").InnerText; - string description = document.GetNodeWith("0a_9").InnerText; + if (document.GetNodeWith("q1_1")?.GetAttributeValue("title", string.Empty) is not { Length: >0 } name) + { + Log.Error("Name not found."); + return null; + } + string description = document.GetNodeWith("0a_9")?.InnerText ?? string.Empty; - string coverRelative = document.GetNodeWith("q1_1").GetAttributeValue("src", ""); - string coverUrl = $"{url.Substring(0, url.IndexOf('/', 9))}{coverRelative}"; + if (document.GetNodeWith("q1_1")?.GetAttributeValue("src", string.Empty) is not { Length: >0 } coverRelative) + { + Log.Error("Cover not found."); + return null; + } + string coverUrl = $"{url[..url.IndexOf('/', 9)]}{coverRelative}"; - MangaReleaseStatus releaseStatus = document.GetNodeWith("Yn_5").InnerText.ToLower() switch + MangaReleaseStatus releaseStatus = document.GetNodeWith("Yn_5")?.InnerText.ToLower() switch { "pending" => MangaReleaseStatus.Unreleased, "ongoing" => MangaReleaseStatus.Continuing, @@ -97,21 +102,24 @@ public class MangaPark : MangaConnector _ => MangaReleaseStatus.Unreleased }; - ICollection authors = document.GetNodeWith("tz_4") + ICollection authors = document.GetNodeWith("tz_4")? .ChildNodes.Where(n => n.Name == "a") .Select(n => n.InnerText) - .Select(t => new Author(t)).ToList(); + .Select(t => new Author(t)) + .ToList()??[]; - ICollection mangaTags = document.GetNodesWith("kd_0") + ICollection mangaTags = document.GetNodesWith("kd_0")? .Select(n => n.InnerText) - .Select(t => new MangaTag(t)).ToList(); + .Select(t => new MangaTag(t)) + .ToList()??[]; ICollection links = []; - ICollection altTitles = document.GetNodeWith("tz_2") + ICollection altTitles = document.GetNodeWith("tz_2")? .ChildNodes.Where(n => n.InnerText.Length > 1) .Select(n => n.InnerText) - .Select(t => new AltTitle(string.Empty, t)).ToList(); + .Select(t => new AltTitle(string.Empty, t)) + .ToList()??[]; Manga m = new (name, description, coverUrl, releaseStatus, authors, mangaTags, links, altTitles); MangaConnectorId mcId = new(m, this, url.Split('/').Last(), url); @@ -136,8 +144,7 @@ public class MangaPark : MangaConnector if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { - HtmlDocument document = new(); - document.Load(result.result); + HtmlDocument document= result.CreateDocument(); HtmlNodeCollection chapterNodes = document.GetNodesWith("8t_8"); @@ -189,8 +196,7 @@ public class MangaPark : MangaConnector if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { - HtmlDocument document = new(); - document.Load(result.result); + HtmlDocument document= result.CreateDocument(); HtmlNodeCollection imageNodes = document.GetNodesWith("8X_2"); @@ -200,8 +206,20 @@ public class MangaPark : MangaConnector } } -internal static class Helper +internal static class MangaParkHelper { - internal static HtmlNode GetNodeWith(this HtmlDocument document, string search, string selector = "q:key") => document.DocumentNode.SelectSingleNode($"//*[@${selector}=${search}]"); - internal static HtmlNodeCollection GetNodesWith(this HtmlDocument document, string search, string selector = "q:key") => document.DocumentNode.SelectNodes($"//*[@${selector}=${search}]"); + internal static HtmlDocument CreateDocument(this RequestResult result) + { + HtmlDocument document = new(); + StreamReader sr = new (result.result); + string htmlStr = sr.ReadToEnd().Replace("q:key", "qkey"); + document.LoadHtml(htmlStr); + + return document; + } + + internal static HtmlNode? GetNodeWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html").GetNodeWith(search); + internal static HtmlNode? GetNodeWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']").FirstOrDefault(); + internal static HtmlNodeCollection GetNodesWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html ").GetNodesWith(search); + internal static HtmlNodeCollection GetNodesWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']"); } \ No newline at end of file diff --git a/API/MangaConnectors/Mangaworld.cs b/API/MangaConnectors/Mangaworld.cs index a6d450c..e8b84cf 100644 --- a/API/MangaConnectors/Mangaworld.cs +++ b/API/MangaConnectors/Mangaworld.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using System.Web; using API.MangaDownloadClients; using API.Schema.MangaContext; using HtmlAgilityPack; @@ -27,7 +28,7 @@ public sealed class Mangaworld : MangaConnector public override (Manga, MangaConnectorId)[] SearchManga(string mangaSearchName) { Uri baseUri = new ("https://www.mangaworld.cx/"); - Uri searchUrl = new (baseUri, "archive?keyword=" + Uri.EscapeDataString(mangaSearchName)); + Uri searchUrl = new (baseUri, "archive?keyword=" + HttpUtility.UrlEncode(mangaSearchName)); RequestResult res = downloadClient.MakeRequest(searchUrl.ToString(), RequestType.Default); if ((int)res.statusCode < 200 || (int)res.statusCode >= 300) From ebb30ff6d7201fcc92397c3f20d09e1fc55c2c11 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 02:16:16 +0200 Subject: [PATCH 03/13] Fix image-url parsing --- API/MangaConnectors/MangaPark.cs | 38 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 772e282..8b04761 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -41,7 +41,7 @@ public class MangaPark : MangaConnector if (document.DocumentNode.SelectSingleNode("//button[contains(text(),\"No Data\")]") is not null) // No results found break; - ret.AddRange(document.GetNodesWith("q4_9").Select(n => ParseSingleMangaFromSearchResultsList(baseUri, n))); + ret.AddRange(document.GetNodesWith("q4_9")?.Select(n => ParseSingleMangaFromSearchResultsList(baseUri, n))??[]); }else return null; } @@ -80,14 +80,14 @@ public class MangaPark : MangaConnector if (document.GetNodeWith("q1_1")?.GetAttributeValue("title", string.Empty) is not { Length: >0 } name) { - Log.Error("Name not found."); + Log.Debug("Name not found."); return null; } string description = document.GetNodeWith("0a_9")?.InnerText ?? string.Empty; if (document.GetNodeWith("q1_1")?.GetAttributeValue("src", string.Empty) is not { Length: >0 } coverRelative) { - Log.Error("Cover not found."); + Log.Debug("Cover not found."); return null; } string coverUrl = $"{url[..url.IndexOf('/', 9)]}{coverRelative}"; @@ -145,8 +145,12 @@ public class MangaPark : MangaConnector { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { HtmlDocument document= result.CreateDocument(); - - HtmlNodeCollection chapterNodes = document.GetNodesWith("8t_8"); + + if (document.GetNodesWith("8t_8") is not { } chapterNodes) + { + Log.Debug("No chapters found."); + return null; + } return chapterNodes.Select(n => ParseChapter(mangaId.Obj, n, baseUri)).ToArray(); } @@ -156,13 +160,13 @@ public class MangaPark : MangaConnector private readonly Regex _volChTitleRex = new(@"(?:.*(?:Vol\.?(?:ume)?)\s*([0-9]+))?.*(?:Ch\.?(?:apter)?)\s*([0-9\.]+)(?::\s+(.*))?"); private (Chapter, MangaConnectorId) ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri) { - HtmlNode linkNode = chapterNode.SelectSingleNode("/div[1]/a"); + HtmlNode linkNode = chapterNode.SelectSingleNode("./div[1]/a"); Match linkMatch = _volChTitleRex.Match(linkNode.InnerText); - HtmlNode? titleNode = chapterNode.SelectSingleNode("/div[1]/span"); + HtmlNode? titleNode = chapterNode.SelectSingleNode("./div[1]/span"); if (!linkMatch.Success || !linkMatch.Groups[2].Success) { - Log.Error($"Unable to parse Chapter: {chapterNode.InnerHtml}"); + Log.Debug($"Unable to parse Chapter: {chapterNode.InnerHtml}"); throw new ($"Unable to parse Chapter: {chapterNode.InnerHtml}"); } @@ -196,11 +200,16 @@ public class MangaPark : MangaConnector if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { - HtmlDocument document= result.CreateDocument(); - - HtmlNodeCollection imageNodes = document.GetNodesWith("8X_2"); + HtmlDocument document = result.CreateDocument(); - return imageNodes.Select(n => n.SelectSingleNode("/div/img").GetAttributeValue("src", "")).ToArray(); + if (document.DocumentNode.SelectSingleNode("//script[@type='qwik/json']")?.InnerText is not { } imageJson) + { + Log.Debug("No images found."); + return null; + } + + MatchCollection matchCollection = Regex.Matches(imageJson, @"https?:\/\/[^,]*\.webp"); + return matchCollection.Select(m => m.Value).ToArray(); } else return null; } @@ -220,6 +229,7 @@ internal static class MangaParkHelper internal static HtmlNode? GetNodeWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html").GetNodeWith(search); internal static HtmlNode? GetNodeWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']").FirstOrDefault(); - internal static HtmlNodeCollection GetNodesWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html ").GetNodesWith(search); - internal static HtmlNodeCollection GetNodesWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']"); + internal static HtmlNodeCollection? GetNodesWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html ").GetNodesWith(search); + // ReSharper disable once ReturnTypeCanBeNotNullable HAP nullable + internal static HtmlNodeCollection? GetNodesWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']"); } \ No newline at end of file From a97fdd9bd710474ad9bca0263bab5a8693f79458 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 02:18:41 +0200 Subject: [PATCH 04/13] Fix MangaPark favicon --- API/MangaConnectors/MangaPark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 8b04761..ba623b5 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -12,7 +12,7 @@ public class MangaPark : MangaConnector public MangaPark() : base("MangaPark", ["en"], ["mangapark.com", "mangapark.net", "mangapark.org", "mangapark.me", "mangapark.io", "mangapark.to", "comicpark.org", "comicpark.to", "readpark.org", "readpark.net", "parkmanga.com", "parkmanga.net", "parkmanga.org", "mpark.to"], - "https://mangapark.com/static-assets/img/favicon.ico") + "/blahaj.png") { this.downloadClient = new HttpDownloadClient(); } From d865116854e3ab5f16477a5531cc4756ac520b17 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 03:05:58 +0200 Subject: [PATCH 05/13] Update Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e00fcc1..dd5f184 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Tranga can download Chapters and Metadata from "Scanlation" sites such as - [MangaDex.org](https://mangadex.org/) (Multilingual) - [MangaWorld](https://www.mangaworld.cx) (it) +- [MangaPark](https://www.mangapark.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+) and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). From d2a56a9d363774004adbb220c651a5dfaee29d99 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 03:17:02 +0200 Subject: [PATCH 06/13] MangaPark fix Tags with only '/' --- API/MangaConnectors/MangaPark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index ba623b5..32e5766 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -116,7 +116,7 @@ public class MangaPark : MangaConnector ICollection links = []; ICollection altTitles = document.GetNodeWith("tz_2")? - .ChildNodes.Where(n => n.InnerText.Length > 1) + .ChildNodes.Where(n => n.InnerText.Trim().Length > 1) .Select(n => n.InnerText) .Select(t => new AltTitle(string.Empty, t)) .ToList()??[]; From 17a36c0429a61a324058f18b497f35f85f30157c Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 04:02:00 +0200 Subject: [PATCH 07/13] MangaPark fix irregular chapter numbering --- API/MangaConnectors/MangaPark.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 32e5766..4a7e0ee 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -4,6 +4,7 @@ using System.Web; using API.MangaDownloadClients; using API.Schema.MangaContext; using HtmlAgilityPack; +using static System.Text.RegularExpressions.Regex; namespace API.MangaConnectors; @@ -163,15 +164,26 @@ public class MangaPark : MangaConnector HtmlNode linkNode = chapterNode.SelectSingleNode("./div[1]/a"); Match linkMatch = _volChTitleRex.Match(linkNode.InnerText); HtmlNode? titleNode = chapterNode.SelectSingleNode("./div[1]/span"); + + string chapterNumber; + int? volumeNumber = null; if (!linkMatch.Success || !linkMatch.Groups[2].Success) { - Log.Debug($"Unable to parse Chapter: {chapterNode.InnerHtml}"); - throw new ($"Unable to parse Chapter: {chapterNode.InnerHtml}"); + Log.Debug($"Not in standard Volume/Chapter format: {chapterNode.InnerText}"); + if (Match(linkNode.InnerText, @"[^\d]*([\d\.]+)[^\d]*") is not { Success: true } match) + { + Log.Debug($"Unable to parse chapter-number: {chapterNode.InnerText}"); + throw new FormatException("Unable to parse chapter-number"); + } + chapterNumber = match.Groups[1].Value; + } + else + { + chapterNumber = linkMatch.Groups[2].Value; + volumeNumber = linkMatch.Groups[1].Success ? int.Parse(linkMatch.Groups[1].Value) : null; } - string chapterNumber = linkMatch.Groups[2].Value; - int? volumeNumber = linkMatch.Groups[1].Success ? int.Parse(linkMatch.Groups[1].Value) : null; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullables string? title = titleNode is not null ? titleNode.InnerText[2..] : (linkMatch.Groups[3].Success ? linkMatch.Groups[3].Value : null); @@ -208,7 +220,7 @@ public class MangaPark : MangaConnector return null; } - MatchCollection matchCollection = Regex.Matches(imageJson, @"https?:\/\/[^,]*\.webp"); + MatchCollection matchCollection = Matches(imageJson, @"https?:\/\/[^,]*\.webp"); return matchCollection.Select(m => m.Value).ToArray(); } else return null; From 5ab61445b6e508ab19e81f8eade7317bbbcb3bb6 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 04:10:44 +0200 Subject: [PATCH 08/13] correct log output --- API/MangaConnectors/MangaPark.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 4a7e0ee..84b3f8a 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -170,10 +170,10 @@ public class MangaPark : MangaConnector if (!linkMatch.Success || !linkMatch.Groups[2].Success) { - Log.Debug($"Not in standard Volume/Chapter format: {chapterNode.InnerText}"); + Log.Debug($"Not in standard Volume/Chapter format: {linkNode.InnerText}"); if (Match(linkNode.InnerText, @"[^\d]*([\d\.]+)[^\d]*") is not { Success: true } match) { - Log.Debug($"Unable to parse chapter-number: {chapterNode.InnerText}"); + Log.Debug($"Unable to parse chapter-number: {linkNode.InnerText}"); throw new FormatException("Unable to parse chapter-number"); } chapterNumber = match.Groups[1].Value; From 88f044eb4cff44824040101fdec41ef58a0a6cb5 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 04:24:40 +0200 Subject: [PATCH 09/13] Fix nullable in GetNodewith --- API/MangaConnectors/MangaPark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 84b3f8a..97feea6 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -240,7 +240,7 @@ internal static class MangaParkHelper } internal static HtmlNode? GetNodeWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html").GetNodeWith(search); - internal static HtmlNode? GetNodeWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']").FirstOrDefault(); + internal static HtmlNode? GetNodeWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']")?.FirstOrDefault(); internal static HtmlNodeCollection? GetNodesWith(this HtmlDocument document, string search) => document.DocumentNode.SelectSingleNode("/html ").GetNodesWith(search); // ReSharper disable once ReturnTypeCanBeNotNullable HAP nullable internal static HtmlNodeCollection? GetNodesWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']"); From 7fa0d9b5ee29fe7c79a6dd45c5a73a1053c3bb57 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 04:29:47 +0200 Subject: [PATCH 10/13] Dont fail if parsing a single chapter fails --- API/MangaConnectors/MangaPark.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index 97feea6..ca8a757 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -142,6 +142,9 @@ public class MangaPark : MangaConnector { Uri baseUri = new ($"https://{domain}/"); Uri requestUri = new (baseUri, $"title/{mangaId.IdOnConnectorSite}"); + + List<(Chapter, MangaConnectorId)> ret = []; + if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result) { @@ -152,14 +155,20 @@ public class MangaPark : MangaConnector Log.Debug("No chapters found."); return null; } - - return chapterNodes.Select(n => ParseChapter(mangaId.Obj, n, baseUri)).ToArray(); + + foreach (HtmlNode chapterNode in chapterNodes) + { + if(ParseChapter(mangaId.Obj, chapterNode, baseUri) is { } ch) + ret.Add(ch); + } } else return null; + + return ret.ToArray(); } private readonly Regex _volChTitleRex = new(@"(?:.*(?:Vol\.?(?:ume)?)\s*([0-9]+))?.*(?:Ch\.?(?:apter)?)\s*([0-9\.]+)(?::\s+(.*))?"); - private (Chapter, MangaConnectorId) ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri) + private (Chapter, MangaConnectorId)? ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri) { HtmlNode linkNode = chapterNode.SelectSingleNode("./div[1]/a"); Match linkMatch = _volChTitleRex.Match(linkNode.InnerText); @@ -174,7 +183,7 @@ public class MangaPark : MangaConnector if (Match(linkNode.InnerText, @"[^\d]*([\d\.]+)[^\d]*") is not { Success: true } match) { Log.Debug($"Unable to parse chapter-number: {linkNode.InnerText}"); - throw new FormatException("Unable to parse chapter-number"); + return null; } chapterNumber = match.Groups[1].Value; } From 4e1c9cd300439c0d00e624b096675478630909fe Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 04:54:18 +0200 Subject: [PATCH 11/13] Fix MangaPark Irregular Format number parsing --- API/MangaConnectors/MangaPark.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index ca8a757..cd3b673 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -167,11 +167,11 @@ public class MangaPark : MangaConnector return ret.ToArray(); } - private readonly Regex _volChTitleRex = new(@"(?:.*(?:Vol\.?(?:ume)?)\s*([0-9]+))?.*(?:Ch\.?(?:apter)?)\s*([0-9\.]+)(?::\s+(.*))?"); + private static readonly Regex VolChTitleRex = new(@"(?:.*(?:Vol\.?(?:ume)?)\s*([0-9]+))?.*(?:Ch\.?(?:apter)?)\s*((?:\d+\.)*[0-9]+)\s*(?::|-\s+(.*))?", RegexOptions.Compiled); private (Chapter, MangaConnectorId)? ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri) { HtmlNode linkNode = chapterNode.SelectSingleNode("./div[1]/a"); - Match linkMatch = _volChTitleRex.Match(linkNode.InnerText); + Match linkMatch = VolChTitleRex.Match(linkNode.InnerText); HtmlNode? titleNode = chapterNode.SelectSingleNode("./div[1]/span"); string chapterNumber; @@ -180,7 +180,7 @@ public class MangaPark : MangaConnector if (!linkMatch.Success || !linkMatch.Groups[2].Success) { Log.Debug($"Not in standard Volume/Chapter format: {linkNode.InnerText}"); - if (Match(linkNode.InnerText, @"[^\d]*([\d\.]+)[^\d]*") is not { Success: true } match) + if (Match(linkNode.InnerText, @"[^\d]*((?:\d+\.)*\d+)[^\d]*") is not { Success: true } match) { Log.Debug($"Unable to parse chapter-number: {linkNode.InnerText}"); return null; From a33c3a2dcc5f09dc95510ea1685bf00015fc50fb Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 05:07:50 +0200 Subject: [PATCH 12/13] Fix parsing of image urls --- API/MangaConnectors/MangaPark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index cd3b673..f9a9cd1 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -229,7 +229,7 @@ public class MangaPark : MangaConnector return null; } - MatchCollection matchCollection = Matches(imageJson, @"https?:\/\/[^,]*\.webp"); + MatchCollection matchCollection = Matches(imageJson, @"https?:\/\/[\da-zA-Z\.]+\/[^,""]*\.[a-z]+"); return matchCollection.Select(m => m.Value).ToArray(); } else return null; From 5dd9bfaefaaa17697c23fedc0a50c574a9ffa562 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 21 Sep 2025 15:15:39 +0200 Subject: [PATCH 13/13] MangaPark HttpUtility.HtmlDecode --- API/MangaConnectors/MangaPark.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/API/MangaConnectors/MangaPark.cs b/API/MangaConnectors/MangaPark.cs index f9a9cd1..6c356d2 100644 --- a/API/MangaConnectors/MangaPark.cs +++ b/API/MangaConnectors/MangaPark.cs @@ -84,7 +84,7 @@ public class MangaPark : MangaConnector Log.Debug("Name not found."); return null; } - string description = document.GetNodeWith("0a_9")?.InnerText ?? string.Empty; + string description = HttpUtility.HtmlDecode(document.GetNodeWith("0a_9")?.InnerText ?? string.Empty); if (document.GetNodeWith("q1_1")?.GetAttributeValue("src", string.Empty) is not { Length: >0 } coverRelative) { @@ -105,12 +105,12 @@ public class MangaPark : MangaConnector ICollection authors = document.GetNodeWith("tz_4")? .ChildNodes.Where(n => n.Name == "a") - .Select(n => n.InnerText) + .Select(n => HttpUtility.HtmlDecode(n.InnerText)) .Select(t => new Author(t)) .ToList()??[]; ICollection mangaTags = document.GetNodesWith("kd_0")? - .Select(n => n.InnerText) + .Select(n => HttpUtility.HtmlDecode(n.InnerText)) .Select(t => new MangaTag(t)) .ToList()??[]; @@ -118,7 +118,7 @@ public class MangaPark : MangaConnector ICollection altTitles = document.GetNodeWith("tz_2")? .ChildNodes.Where(n => n.InnerText.Trim().Length > 1) - .Select(n => n.InnerText) + .Select(n => HttpUtility.HtmlDecode(n.InnerText)) .Select(t => new AltTitle(string.Empty, t)) .ToList()??[]; @@ -171,7 +171,8 @@ public class MangaPark : MangaConnector private (Chapter, MangaConnectorId)? ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri) { HtmlNode linkNode = chapterNode.SelectSingleNode("./div[1]/a"); - Match linkMatch = VolChTitleRex.Match(linkNode.InnerText); + string linkNodeText = HttpUtility.HtmlDecode(linkNode.InnerText); + Match linkMatch = VolChTitleRex.Match(linkNodeText); HtmlNode? titleNode = chapterNode.SelectSingleNode("./div[1]/span"); string chapterNumber; @@ -179,10 +180,10 @@ public class MangaPark : MangaConnector if (!linkMatch.Success || !linkMatch.Groups[2].Success) { - Log.Debug($"Not in standard Volume/Chapter format: {linkNode.InnerText}"); - if (Match(linkNode.InnerText, @"[^\d]*((?:\d+\.)*\d+)[^\d]*") is not { Success: true } match) + Log.Debug($"Not in standard Volume/Chapter format: {linkNodeText}"); + if (Match(linkNodeText, @"[^\d]*((?:\d+\.)*\d+)[^\d]*") is not { Success: true } match) { - Log.Debug($"Unable to parse chapter-number: {linkNode.InnerText}"); + Log.Debug($"Unable to parse chapter-number: {linkNodeText}"); return null; } chapterNumber = match.Groups[1].Value; @@ -194,7 +195,7 @@ public class MangaPark : MangaConnector } // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullables - string? title = titleNode is not null ? titleNode.InnerText[2..] : (linkMatch.Groups[3].Success ? linkMatch.Groups[3].Value : null); + string? title = titleNode is not null ? HttpUtility.HtmlDecode(titleNode.InnerText)[2..] : (linkMatch.Groups[3].Success ? linkMatch.Groups[3].Value : null); string url = new Uri(baseUri, linkNode.GetAttributeValue("href", "")).ToString(); string id = linkNode.GetAttributeValue("href", "")[7..];