From f4d199565d33e68f1b0740c40e93f8b92dc5c01d Mon Sep 17 00:00:00 2001 From: clfws9 Date: Fri, 21 Feb 2025 04:52:53 +0900 Subject: [PATCH] Add: MangaUP! (Japan) (#1030) * Add: MangaUP! (Japan) * MangaUpJapan: fix text encoding * MangaUpJapan: code refactoring * MangaUp: remove unnecessary line break * MangaUpJapan: code refactoring * MangaUpJapan: code refactoring * MangaUpJapan: use the MangaCSS decorator * MangaUpJapan: convert full-width spaces etc. in chapter titles to half-width spaces --- web/src/engine/websites/MangaUpJapan.ts | 80 ++++++++++++++++++++ web/src/engine/websites/MangaUpJapan.webp | Bin 0 -> 2570 bytes web/src/engine/websites/MangaUpJapan_e2e.ts | 22 ++++++ web/src/engine/websites/_index.ts | 1 + 4 files changed, 103 insertions(+) create mode 100644 web/src/engine/websites/MangaUpJapan.ts create mode 100644 web/src/engine/websites/MangaUpJapan.webp create mode 100644 web/src/engine/websites/MangaUpJapan_e2e.ts diff --git a/web/src/engine/websites/MangaUpJapan.ts b/web/src/engine/websites/MangaUpJapan.ts new file mode 100644 index 0000000000..46ebc2b51d --- /dev/null +++ b/web/src/engine/websites/MangaUpJapan.ts @@ -0,0 +1,80 @@ +import { Tags } from '../Tags'; +import icon from './MangaUpJapan.webp'; +import { Chapter, DecoratableMangaScraper, type Manga, Page } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import { FetchCSS } from '../platform/FetchProvider'; + +type JSONChapter = { + id: number, + name: string, + subName: string +} + +type JSONPage = { + content: { + value: { + imageUrl: string, + } + } +} + +const mangasEndpoints = [ + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', + 'sun', + 'end', + 'yomikiri' +].map(slug => `/series/${slug}`); + +function MangasExtractor(element: HTMLAnchorElement) { + return { + id: element.pathname, + title: element.querySelector('div.pc\\:text-title-md-pc').innerText + }; +} + +@Common.MangaCSS(/^{origin}\/titles\/\d+$/, 'h2.pc\\:text-title-lg-pc') +@Common.MangasSinglePagesCSS(mangasEndpoints, 'a:has(div.pc\\:text-title-md-pc)', MangasExtractor) +@Common.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('mangaupjapan', 'MangaUp (マンガアップ!)', 'https://www.manga-up.com', Tags.Language.Japanese, Tags.Source.Official, Tags.Media.Manga); + } + + public override get Icon(): string { + return icon; + } + + private ExtractNextJsPayloadData(element: HTMLScriptElement): string { + const data = element.innerHTML.match(/^self\.__next_f\.push\(\[\d+,"(.*)"\]\)$/)?.at(1); + return data?.replace(/\\{1,2}"/g, '"').replace(/\\{2,3}n/g, '\\n'); + } + + private async ExtractJSONData(uri: URL, scriptMatcher: string, dataRegexp: RegExp): Promise { + const elements = await FetchCSS(new Request(uri.href), 'script:not([src])'); + for (const element of elements) { + const data = this.ExtractNextJsPayloadData(element); + if (data?.includes(scriptMatcher)) { + return JSON.parse(data.match(dataRegexp).at(1)) as T; + } + } + return undefined; + } + + public override async FetchChapters(manga: Manga): Promise { + const chapters = await this.ExtractJSONData(new URL(manga.Identifier, this.URI), '"chapters":[', /"chapters":(\[{.*?}\]),/); + return chapters.map(({ id, name, subName }) => new Chapter(this, manga, `${manga.Identifier}/chapters/${id}`, `${subName} ${name}`.replace(/\s+/g, ' ').trim())); + } + + public override async FetchPages(chapter: Chapter): Promise { + const pages = await this.ExtractJSONData(new URL(chapter.Identifier, this.URI), '[{"content":', /(\[{"content":.*?}}}\]),/); + return pages + .map(page => page.content.value.imageUrl) + .filter(imageUrl => imageUrl) + .map(imageUrl => new Page(this, chapter, new URL(imageUrl))); + } +} diff --git a/web/src/engine/websites/MangaUpJapan.webp b/web/src/engine/websites/MangaUpJapan.webp new file mode 100644 index 0000000000000000000000000000000000000000..a0ae1badeb43bd13953da9bc427fad56a4585bc6 GIT binary patch literal 2570 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