mirror of
https://github.com/C9Glax/tranga.git
synced 2025-10-11 05:09:49 +02:00
@@ -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;
|
||||
|
@@ -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;
|
||||
|
257
API/MangaConnectors/MangaPark.cs
Normal file
257
API/MangaConnectors/MangaPark.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaContext;
|
||||
using HtmlAgilityPack;
|
||||
using static System.Text.RegularExpressions.Regex;
|
||||
|
||||
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"],
|
||||
"/blahaj.png")
|
||||
{
|
||||
this.downloadClient = new HttpDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
foreach (string uri in BaseUris)
|
||||
if (SearchMangaWithDomain(mangaSearchName, uri) is { } result)
|
||||
return result;
|
||||
return [];
|
||||
}
|
||||
|
||||
private (Manga, MangaConnectorId<Manga>)[]? SearchMangaWithDomain(string mangaSearchName, string domain)
|
||||
{
|
||||
Uri baseUri = new($"https://{domain}/");
|
||||
|
||||
List<(Manga, MangaConnectorId<Manga>)> ret = [];
|
||||
|
||||
for (int page = 1;; page++) // break; in loop
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
ret.AddRange(document.GetNodesWith("q4_9")?.Select(n => ParseSingleMangaFromSearchResultsList(baseUri, n))??[]);
|
||||
}else
|
||||
return null;
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
private (Manga, MangaConnectorId<Manga>) ParseSingleMangaFromSearchResultsList(Uri baseUri, HtmlNode resultNode)
|
||||
{
|
||||
HtmlNode titleAndLinkNode = resultNode.SelectSingleNode("//a[contains(@href,'title')]");
|
||||
string link = titleAndLinkNode.Attributes["href"].Value;
|
||||
|
||||
return ((Manga, MangaConnectorId<Manga>))GetMangaFromUrl(new Uri(baseUri, link).ToString())!;
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
foreach (string uri in BaseUris)
|
||||
if (GetMangaFromIdWithDomain(mangaIdOnSite, uri) is { } result)
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
private (Manga, MangaConnectorId<Manga>)? GetMangaFromIdWithDomain(string mangaIdOnSite, string domain)
|
||||
{
|
||||
Uri baseUri = new ($"https://{domain}/");
|
||||
return GetMangaFromUrl(new Uri(baseUri, $"title/{mangaIdOnSite}").ToString());
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
||||
{
|
||||
if (downloadClient.MakeRequest(url, RequestType.Default) is
|
||||
{ statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
||||
{
|
||||
HtmlDocument document= result.CreateDocument();
|
||||
|
||||
if (document.GetNodeWith("q1_1")?.GetAttributeValue("title", string.Empty) is not { Length: >0 } name)
|
||||
{
|
||||
Log.Debug("Name not found.");
|
||||
return null;
|
||||
}
|
||||
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)
|
||||
{
|
||||
Log.Debug("Cover not found.");
|
||||
return null;
|
||||
}
|
||||
string coverUrl = $"{url[..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<Author> authors = document.GetNodeWith("tz_4")?
|
||||
.ChildNodes.Where(n => n.Name == "a")
|
||||
.Select(n => HttpUtility.HtmlDecode(n.InnerText))
|
||||
.Select(t => new Author(t))
|
||||
.ToList()??[];
|
||||
|
||||
ICollection<MangaTag> mangaTags = document.GetNodesWith("kd_0")?
|
||||
.Select(n => HttpUtility.HtmlDecode(n.InnerText))
|
||||
.Select(t => new MangaTag(t))
|
||||
.ToList()??[];
|
||||
|
||||
ICollection<Link> links = [];
|
||||
|
||||
ICollection<AltTitle> altTitles = document.GetNodeWith("tz_2")?
|
||||
.ChildNodes.Where(n => n.InnerText.Trim().Length > 1)
|
||||
.Select(n => HttpUtility.HtmlDecode(n.InnerText))
|
||||
.Select(t => new AltTitle(string.Empty, t))
|
||||
.ToList()??[];
|
||||
|
||||
Manga m = new (name, description, coverUrl, releaseStatus, authors, mangaTags, links, altTitles);
|
||||
MangaConnectorId<Manga> mcId = new(m, this, url.Split('/').Last(), url);
|
||||
m.MangaConnectorIds.Add(mcId);
|
||||
return (m, mcId);
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
|
||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId, string? language = null)
|
||||
{
|
||||
foreach (string uri in BaseUris)
|
||||
if (GetChaptersFromDomain(mangaId, uri) is { } result)
|
||||
return result;
|
||||
return [];
|
||||
}
|
||||
|
||||
private (Chapter, MangaConnectorId<Chapter>)[]? GetChaptersFromDomain(MangaConnectorId<Manga> mangaId, string domain)
|
||||
{
|
||||
Uri baseUri = new ($"https://{domain}/");
|
||||
Uri requestUri = new (baseUri, $"title/{mangaId.IdOnConnectorSite}");
|
||||
|
||||
List<(Chapter, MangaConnectorId<Chapter>)> ret = [];
|
||||
|
||||
if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is
|
||||
{ statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
||||
{
|
||||
HtmlDocument document= result.CreateDocument();
|
||||
|
||||
if (document.GetNodesWith("8t_8") is not { } chapterNodes)
|
||||
{
|
||||
Log.Debug("No chapters found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (HtmlNode chapterNode in chapterNodes)
|
||||
{
|
||||
if(ParseChapter(mangaId.Obj, chapterNode, baseUri) is { } ch)
|
||||
ret.Add(ch);
|
||||
}
|
||||
}
|
||||
else return null;
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
private static readonly Regex VolChTitleRex = new(@"(?:.*(?:Vol\.?(?:ume)?)\s*([0-9]+))?.*(?:Ch\.?(?:apter)?)\s*((?:\d+\.)*[0-9]+)\s*(?::|-\s+(.*))?", RegexOptions.Compiled);
|
||||
private (Chapter, MangaConnectorId<Chapter>)? ParseChapter(Manga manga, HtmlNode chapterNode, Uri baseUri)
|
||||
{
|
||||
HtmlNode linkNode = chapterNode.SelectSingleNode("./div[1]/a");
|
||||
string linkNodeText = HttpUtility.HtmlDecode(linkNode.InnerText);
|
||||
Match linkMatch = VolChTitleRex.Match(linkNodeText);
|
||||
HtmlNode? titleNode = chapterNode.SelectSingleNode("./div[1]/span");
|
||||
|
||||
string chapterNumber;
|
||||
int? volumeNumber = null;
|
||||
|
||||
if (!linkMatch.Success || !linkMatch.Groups[2].Success)
|
||||
{
|
||||
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: {linkNodeText}");
|
||||
return null;
|
||||
}
|
||||
chapterNumber = match.Groups[1].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
chapterNumber = linkMatch.Groups[2].Value;
|
||||
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 ? 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..];
|
||||
|
||||
Chapter chapter = new (manga, chapterNumber, volumeNumber, title);
|
||||
MangaConnectorId<Chapter> chId = new(chapter, this, id, url);
|
||||
chapter.MangaConnectorIds.Add(chId);
|
||||
|
||||
return (chapter, chId);
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
||||
{
|
||||
foreach (string uri in BaseUris)
|
||||
if (GetChapterImageUrlsFromDomain(chapterId, uri) is { } result)
|
||||
return result;
|
||||
return [];
|
||||
}
|
||||
|
||||
private string[]? GetChapterImageUrlsFromDomain(MangaConnectorId<Chapter> 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 = result.CreateDocument();
|
||||
|
||||
if (document.DocumentNode.SelectSingleNode("//script[@type='qwik/json']")?.InnerText is not { } imageJson)
|
||||
{
|
||||
Log.Debug("No images found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
MatchCollection matchCollection = Matches(imageJson, @"https?:\/\/[\da-zA-Z\.]+\/[^,""]*\.[a-z]+");
|
||||
return matchCollection.Select(m => m.Value).ToArray();
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class MangaParkHelper
|
||||
{
|
||||
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);
|
||||
// ReSharper disable once ReturnTypeCanBeNotNullable HAP nullable
|
||||
internal static HtmlNodeCollection? GetNodesWith(this HtmlNode node, string search) => node.SelectNodes($"{node.XPath}//*[@qkey='{search}']");
|
||||
}
|
@@ -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<Manga>)[] 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)
|
||||
|
@@ -23,6 +23,7 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
||||
.HasDiscriminator(c => c.Name)
|
||||
.HasValue<Global>("Global")
|
||||
.HasValue<MangaDex>("MangaDex")
|
||||
.HasValue<MangaPark>("MangaPark")
|
||||
.HasValue<Mangaworld>("Mangaworld");
|
||||
|
||||
//Manga has many Chapters
|
||||
|
@@ -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
|
||||
|
@@ -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/).
|
||||
|
@@ -2,6 +2,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=altnames/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=authorsartists/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=comick/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=comicpark/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gotify/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=jikan/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=jjob/@EntryIndexedValue">True</s:Boolean>
|
||||
@@ -11,9 +12,13 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangaconnector/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangapark/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangaworld/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mpark/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ntfy/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=parkmanga/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=readpark/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=solverr/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taskmanager/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean>
|
||||
|
Reference in New Issue
Block a user