Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MangaUpGlobal : use API & protobuff #914

Merged
merged 3 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions web/src/engine/websites/MangaUpGlobal.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package MangaUpGlobal;
syntax = "proto3";


MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
message SearchView {
repeated Manga titles = 2;
}

message Manga {
optional uint32 titleId = 1;
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
optional string titleName = 2;
}

message MangaDetailView {
optional string titleName = 3;
repeated Chapter chapters = 13;
}


message Chapter {
optional uint32 id = 1;
optional string titleName = 2;
optional string subName = 3;
}

message MangaViewerV2View {
repeated PageBlock pageblocks = 3;
}

message PageBlock {
repeated MangaPage pages = 3;
}

message MangaPage {
optional string imageUrl = 1;
optional string encryptionKey = 5;
optional string iv = 6;
}
91 changes: 62 additions & 29 deletions web/src/engine/websites/MangaUpGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,52 @@ import { Tags } from '../Tags';
import icon from './MangaUpGlobal.webp';
import { Chapter, DecoratableMangaScraper, Manga, Page, type MangaPlugin } from '../providers/MangaPlugin';
import * as Common from './decorators/Common';
import { FetchWindowScript } from '../platform/FetchProvider';
import { FetchProto } from '../platform/FetchProvider';
import { Exception } from '../Error';
import { WebsiteResourceKey as W } from '../../i18n/ILocale';
import protoTypes from './MangaUpGlobal.proto?raw';
import type { Priority } from '../taskpool/DeferredTask';
import { FromHexString } from '../BufferEncoder';
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved

type NEXTDATA<T> = {
props: {
pageProps: {
data: T
}
}
type APIMangaDetailView = {
titleName: string,
chapters: APIChapter[]
}

type APIMangas = {
type APISearch = {
titles: APIManga[]
}

type APIManga = {
titleId: string,
titleName: string,
chapters?: APIChapter[]
titleId: number,
titleName: string
}

type APIChapter = {
id: number,
mainName: string
titleName: string,
subName: string;
}

type APIPages = {
pages: {
imageUrl: string
pageblocks: {
pages: {
imageUrl: string,
encryptionKey: string,
iv: string | undefined
}[]
}[]
}

@Common.ImageAjax()
type CryptoParams = {
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
method: string | undefined,
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
key: string,
iv: string | undefined
}

export default class extends DecoratableMangaScraper {
private readonly apiUrl = 'https://global-api.manga-up.com/api/';
private readonly imagesCDN = 'https://global-img.manga-up.com/';

public constructor() {
super('mangaupglobal', `MangaUp (Global)`, `https://global.manga-up.com`, Tags.Language.English, Tags.Media.Manga, Tags.Source.Official);
Expand All @@ -51,29 +62,51 @@ export default class extends DecoratableMangaScraper {

public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
const mangaid = url.split('/').at(-1);
const data = await this.FetchNextData<APIManga>(new URL(`/manga/${mangaid}`, this.URI));
return new Manga(this, provider, mangaid, data.titleName.trim());
const request = new Request(new URL(`manga/detail_v2?title_id=${mangaid}`, this.apiUrl));
const data = await FetchProto<APIMangaDetailView>(request, protoTypes, 'MangaUpGlobal.MangaDetailView');
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
return new Manga(this, provider, mangaid, data.titleName);
}

public override async FetchMangas(provider: MangaPlugin): Promise<Manga[]> {
const data = await this.FetchNextData<APIMangas>(new URL('/search', this.URI));
return data.titles.map(manga => new Manga(this, provider, manga.titleId, manga.titleName.trim()));
const request = new Request(new URL(`search`, this.apiUrl));
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
const { titles } = await FetchProto<APISearch>(request, protoTypes, 'MangaUpGlobal.SearchView');
return titles.map(manga => new Manga(this, provider, manga.titleId.toString(), manga.titleName.trim()));
}

public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
const data = await this.FetchNextData<APIManga>(new URL(`/manga/${manga.Identifier}`, this.URI));
return data.chapters.map(chapter => new Chapter(this, manga, chapter.id.toString(), chapter.mainName.trim()));
const request = new Request(new URL(`manga/detail_v2?title_id=${manga.Identifier}`, this.apiUrl));
const { chapters } = await FetchProto<APIMangaDetailView>(request, protoTypes, 'MangaUpGlobal.MangaDetailView');
return chapters.map(chapter => new Chapter(this, manga, chapter.id.toString(), [chapter.titleName, chapter.subName].join(' ').trim()));
}

public override async FetchPages(chapter: Chapter): Promise<Page[]> {
const data = await this.FetchNextData<APIPages>(new URL(`/manga/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI));
if (!data)
throw new Exception(W.Plugin_Common_Chapter_UnavailableError);
return data.pages.map(image => new Page(this, chapter, new URL(image.imageUrl)));
public override async FetchPages(chapter: Chapter): Promise<Page<CryptoParams>[]> {
const request = new Request(new URL(`manga/viewer_v2?chapter_id=${chapter.Identifier}&quality=high`, this.apiUrl), { method: 'POST' });
const data = await FetchProto<APIPages>(request, protoTypes, 'MangaUpGlobal.MangaViewerV2View');

if (!data.pageblocks) throw new Exception(W.Plugin_Common_Chapter_UnavailableError);

return data.pageblocks.shift().pages.map(page => {
const params: CryptoParams = {
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
method: page.iv ? 'aes-cbc' : undefined,
key: page.encryptionKey,
iv: page.iv
};
return new Page<CryptoParams>(this, chapter, new URL(page.imageUrl, this.imagesCDN), params);
});
}

private async FetchNextData<T extends JSONElement>(url: URL): Promise<T> {
const data = await FetchWindowScript<NEXTDATA<T>>(new Request(url), '__NEXT_DATA__', 2000);
return data.props.pageProps.data as T;
public override async FetchImage(page: Page<CryptoParams>, priority: Priority, signal: AbortSignal): Promise<Blob> {
const blob = await Common.FetchImageAjax.call(this, page, priority, signal, true);
const cryptoParams = page.Parameters;
switch (cryptoParams.method) {
MikeZeDev marked this conversation as resolved.
Show resolved Hide resolved
case 'aes-cbc': {
const encrypted = await blob.arrayBuffer();
const cipher = { name: 'AES-CBC', iv: FromHexString(cryptoParams.iv) };
const cryptoKey = await crypto.subtle.importKey('raw', FromHexString(cryptoParams.key), cipher, false, ['decrypt']);
const decrypted = await crypto.subtle.decrypt(cipher, cryptoKey, encrypted);
return Common.GetTypedData(decrypted);
}
default: return blob;
}
}
}
Loading