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

Mangalib : rewrite website #877

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
143 changes: 111 additions & 32 deletions web/src/engine/websites/MangaLib.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,125 @@
import { Tags } from '../Tags';
import icon from './MangaLib.webp';
import { DecoratableMangaScraper } from '../providers/MangaPlugin';
import { Chapter, DecoratableMangaScraper, Manga, type MangaPlugin, Page } from '../providers/MangaPlugin';
import * as Common from './decorators/Common';
import { FetchJSON } from '../platform/FetchProvider';
import { Priority, TaskPool } from '../taskpool/TaskPool';
import { RateLimit } from '../taskpool/RateLimit';

function MangaExtractor(anchor: HTMLAnchorElement) {
return {
id: anchor.pathname,
title: anchor.querySelector('.media-card__title').textContent.trim()
};
}

const chapterScript = `
new Promise(resolve => {
const chapters = window.__DATA__.chapters.list.map(entry => {
return {
id: '/' + window.__DATA__.manga.slug + '/v' + entry.chapter_volume + '/c' + entry.chapter_number,
title: entry.chapter_number + (entry.chapter_name ? ' - ' + entry.chapter_name : '')
};
});
resolve(chapters);
});
`;

const pageScript = `
new Promise(resolve => {
resolve(window.__pg.map(page => window.__info.servers.main + window.__info.img.url + page.u));
});
`;

@Common.MangaCSS(/^{origin}\/[^/]+(\?section=info)?$/, 'div.media-name__body div.media-name__main')
@Common.MangasMultiPageCSS('/manga-list?page={page}', 'div.media-card-wrap a.media-card', 1, 1, 0, MangaExtractor)
@Common.ChaptersSinglePageJS(chapterScript, 500)
@Common.PagesSinglePageJS(pageScript, 500)
@Common.ImageAjax()
type ImageServers = {
imageServers: ImageServer[]
}

type ImageServer = {
id: string,
url: string,
site_ids: number[]
}

type APIResult<T> = {
data: T,
meta: {
has_next_page: boolean,
}
}

type APIManga = {
id: number,
rus_name: string,
slug_url: string
}

type APIChapter = {
volume: string,
number: string,
name: string,
branches_count: number,
branches: {
id: number,
branch_id: number | null,
teams: {
name: string
}[]
}[]
}

type APIPages = {
pages: {
url: string
}[]
}

type ChapterID = {
branch_id: string,
number: string,
volume: string
}

@Common.ImageAjax(true)
export default class extends DecoratableMangaScraper {
private readonly apiUrl = 'https://api.mangalib.me/api/';
private readonly siteId = 1;
private imageServer: ImageServer = undefined;
private readonly mangasTaskPool = new TaskPool(1, new RateLimit(2, 1));

public constructor() {
super('mangalib', 'MangaLib', 'https://mangalib.org', Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Language.Russian, Tags.Source.Aggregator);
super('mangalib', 'MangaLib', 'https://mangalib.org', Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Manga, Tags.Language.Russian, Tags.Source.Aggregator);
}

public override get Icon() {
return icon;
}

public override async Initialize(): Promise<void> {
const { data: { imageServers } } = await FetchJSON<APIResult<ImageServers>>(new Request(new URL('constants?fields[]=imageServers', this.apiUrl)));
this.imageServer = imageServers.find(server => server.id === 'main' && server.site_ids.includes(this.siteId));
}

public override ValidateMangaURL(url: string): boolean {
return new RegExpSafe(`^${this.URI.origin}/ru/manga/[^/]`).test(url);
}

public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
const mangaSlug = new URL(url).pathname.split('/').at(-1);
const { data: { slug_url, rus_name } } = await FetchJSON<APIResult<APIManga>>(new Request(new URL(`manga/${mangaSlug}`, this.apiUrl)));
return new Manga(this, provider, slug_url, rus_name);
}

public override async FetchMangas(provider: MangaPlugin): Promise<Manga[]> {
const mangaList: Manga[] = [];
for (let page = 1, run = true; run; page++) {
const url = new URL(`manga?page=${page}&site_id[]=${this.siteId}`, this.apiUrl);
const { data, meta: { has_next_page } } = await this.mangasTaskPool.Add(() => FetchJSON<APIResult<APIManga[]>>(new Request(url)), Priority.Low);
mangaList.push(...data.map(manga => new Manga(this, provider, manga.slug_url, manga.rus_name.trim())));
run = has_next_page;
}
return mangaList;
}

public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
const { data } = await FetchJSON<APIResult<APIChapter[]>>(new Request(new URL(`manga/${manga.Identifier}/chapters`, this.apiUrl)));
return data.reduce((accumulator: Chapter[], chapter) => {
let baseTitle = `Том ${chapter.volume} Глава ${chapter.number}`;
baseTitle += chapter.name ? ` - ${chapter.name}` : '';
const chapters = chapter.branches.map(branch => {
const teamName = chapter.branches_count > 1 ? `[${branch.teams.at(0).name}]` : '';
const chapterId = JSON.stringify({
branch_id: branch.branch_id?.toString() ?? '',
number: chapter.number,
volume: chapter.volume
});
return new Chapter(this, manga, chapterId, [baseTitle, teamName].join(' ').trim());
});
accumulator.push(...chapters);
return accumulator;
}, []);
}

public override async FetchPages(chapter: Chapter): Promise<Page[]> {
const { branch_id, number, volume } = JSON.parse(chapter.Identifier) as ChapterID;
const url = new URL(`manga/${chapter.Parent.Identifier}/chapter?number=${number}&volume=${volume}`, this.apiUrl);
if (branch_id) url.searchParams.set('branch_id', branch_id);
const { data: { pages } } = await FetchJSON<APIResult<APIPages>>(new Request(url));
return pages.map(page => new Page(this, chapter, new URL(this.imageServer.url + page.url)));
}
}
43 changes: 33 additions & 10 deletions web/src/engine/websites/MangaLib_e2e.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
import { TestFixture } from '../../../test/WebsitesFixture';

const config = {
const configWithBranches = {
plugin: {
id: 'mangalib',
title: 'MangaLib'
},
container: {
url: 'https://mangalib.org/toukyou-revengers',
id: '/toukyou-revengers',
title: 'Токийские мстители'
url: 'https://mangalib.org/ru/manga/7965--chainsaw-man',
id: '7965--chainsaw-man',
title: 'Человек-бензопила'
},
child: {
id: '/toukyou-revengers/v1/c1',
title: '1 - Reborn'
id: JSON.stringify({ branch_id: '4667', number: '1', volume: '1' }),
title: 'Том 1 Глава 1 - Пёс и бензопила [Nippa Team]'
},
entry: {
index: 1,
size: 100_108,
type: 'image/jpeg'
index: 4,
size: 314_272,
type: 'image/webp'
}
};

new TestFixture(config).AssertWebsite();
new TestFixture(configWithBranches).AssertWebsite();

const configWithoutBranches = {
plugin: {
id: 'mangalib',
title: 'MangaLib'
},
container: {
url: 'https://mangalib.org/ru/manga/210908--arlokk-the-atrocious',
id: '210908--arlokk-the-atrocious',
title: 'Злыдень Арлокк'
},
child: {
id: JSON.stringify({ branch_id: '', number: '1', volume: '1' }),
title: 'Том 1 Глава 1 - Встреча со злодеем'
},
entry: {
index: 0,
size: 126_508,
type: 'image/webp'
}
};

new TestFixture(configWithoutBranches).AssertWebsite();