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

ADD VIZ ShonenJump #747

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions package.json
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,9 @@
"test:e2e": "npm run build --workspaces && vitest run --config=test/vitest.e2e.js",
"test:websites": "npm run build --workspaces && vitest run --config=test/vitest.websites.js",
"npm:clean-install": "npm update --package-lock-only && npm ci"

},
"dependencies": {
"exifr": "^7.1.3"
}
}
156 changes: 156 additions & 0 deletions web/src/engine/websites/VizShonenJump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Tags } from '../Tags';
import icon from './VizShonenJump.webp';
import { type Chapter, DecoratableMangaScraper, type Manga, Page, type MangaPlugin } from '../providers/MangaPlugin';
import { Fetch, FetchWindowScript } from '../platform/FetchProvider';
import type { Priority } from '../taskpool/DeferredTask';
import * as Common from './decorators/Common';
import exifr from 'exifr';
import DeScramble from '../transformers/ImageDescrambler';
import { RateLimit } from '../taskpool/RateLimit';

type PagesInfos = {
pagesCount: number,
mangaID: string
}

type ExifData = {
ImageUniqueID: string,
ImageWidth: number,
ImageHeight: number
}

const PagesScript = `
new Promise ( resolve => {
resolve({
pagesCount : pages,
mangaID : mangaCommonId ?? currentMCid
});
});
`;

const MangasExtractor = Common.AnchorInfoExtractor(false, '.display-label');

function VolumesExtractor(row: HTMLTableRowElement) {
const anchor = row.querySelector<HTMLAnchorElement>('a.btn-primary-dark');
return {
id: anchor.pathname + anchor.search,
title: row.querySelector<HTMLTableCellElement>('td.product-table--primary').textContent.replace(', Vol.', 'Vol.').trim()
};
}

function ChapterExtractor(anchor: HTMLAnchorElement) {
return {
id: /javascript/.test(anchor.dataset.targetUrl) ? anchor.dataset.targetUrl.match(/['"](\/(shonenjump|vizmanga)[^']+)['"]/)[1] : anchor.dataset.targetUrl,
title: (anchor.querySelector<HTMLElement>('.disp-id, tr.o_chapter td > div')?.textContent ?? anchor.text).trim()
};
}

export default class extends DecoratableMangaScraper {

public constructor() {
super('vizshonenjump', 'Viz - Shonen Jump', 'https://www.viz.com', Tags.Language.English, Tags.Media.Manga, Tags.Source.Official, Tags.Accessibility.RegionLocked);
this.imageTaskPool.RateLimit = new RateLimit(4, 1);
}

public override get Icon() {
return icon;
}

public override ValidateMangaURL(url: string): boolean {
const mangaRegexp = new RegExpSafe(`^${this.URI.origin}/(shonenjump|vizmanga)/chapters/[^/]+$`);
const libraryRegexp = new RegExpSafe(`^${this.URI.origin}/account/library/(gn|sj)/[^/]+$`);
return mangaRegexp.test(url) || libraryRegexp.test(url);
}

public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
const mangaRegexp = new RegExpSafe(`^${this.URI.origin}/(shonenjump|vizmanga)/chapters/[^/]+$`);
return mangaRegexp.test(url) ? await Common.FetchMangaCSS.call(this, provider, url, 'section#series-intro div h2') : await Common.FetchMangaCSS.call(this, provider, url, 'body > div.row h3.type-md');
}

public override async FetchMangas(provider: MangaPlugin): Promise<Manga[]> {
return [
...await Common.FetchMangasSinglePageCSS.call(this, provider, '/account/library', 'table.purchase-table a', MangasExtractor),
...await Common.FetchMangasSinglePageCSS.call(this, provider, '/account/library/sj', 'table.purchase-table a', MangasExtractor),
...await Common.FetchMangasSinglePageCSS.call(this, provider, '/read/shonenjump/section/free-chapters', 'div#chpt_grid div.o_sortable a.o_chapters-link', MangasExtractor),
...await Common.FetchMangasSinglePageCSS.call(this, provider, '/read/vizmanga/section/free-chapters', 'div.o_sort_container div.o_sortable a.o_chapters-link', MangasExtractor)
].distinct();
}

public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
return /^\/(shonenjump|vizmanga)\/chapters/.test(manga.Identifier)
? Common.FetchChaptersSinglePageCSS.call(this, manga, 'div > a.o_chapter-container[data-target-url]:not([href*="javascript"]), tr.o_chapter td.ch-num-list-spacing a.o_chapter-container[data-target-url]:not([href*="javascript"])', ChapterExtractor)
: Common.FetchChaptersSinglePageCSS.call(this, manga, 'table.product-table tr', VolumesExtractor);
}

public override async FetchPages(chapter: Chapter): Promise<Page[]> {
const chapterurl = new URL(chapter.Identifier, this.URI);
const { pagesCount, mangaID } = await FetchWindowScript<PagesInfos>(new Request(chapterurl), PagesScript, 1500);
return Array(pagesCount + 1).fill(0).map((_, index) => {
const url = new URL('/manga/get_manga_url', this.URI);
url.searchParams.set('device_id', '3');
url.searchParams.set('manga_id', mangaID);
url.searchParams.set('page', index.toString());
return new Page(this, chapter, url, { Referer: chapterurl.href });
});
}

public override async FetchImage(page: Page, priority: Priority, signal: AbortSignal): Promise<Blob> {
const buffer = await this.imageTaskPool.Add(async () => {
let response = await Fetch(new Request(page.Link, {
signal,
headers: {
Referer: page.Parameters.Referer,
Origin: this.URI.origin,
}
}));

const img_url = await response.text();
response = await Fetch(new Request(img_url, {
signal,
headers: {
Referer: page.Parameters.Referer,
Origin: this.URI.origin,
crossOrigin: 'Anonymous'
}
}));
return await response.arrayBuffer();

}, priority, signal);

const tags: ExifData = await exifr.parse(buffer);
const EXIFWIDTH = tags.ImageWidth;
const EXIFHEIGHT = tags.ImageHeight;
const shuffleMap = tags.ImageUniqueID.split(':');

return DeScramble(new ImageData(EXIFWIDTH, EXIFHEIGHT), async (_, ctx) => {
const blob = await Common.GetTypedData(buffer);
const bitmap = await createImageBitmap(blob);
const x_split = Math.floor(EXIFWIDTH / 10);
const y_split = Math.floor(EXIFHEIGHT / 15);

ctx.clearRect(0, 0, EXIFWIDTH, EXIFHEIGHT);

//Draw borders
ctx.drawImage(bitmap, 0, 0, EXIFWIDTH, y_split, 0, 0, EXIFWIDTH, y_split);
ctx.drawImage(bitmap, 0, y_split + 10, x_split, EXIFHEIGHT - 2 * y_split, 0, y_split, x_split, EXIFHEIGHT - 2 * y_split);
ctx.drawImage(bitmap, 0, 14 * (y_split + 10), EXIFWIDTH, bitmap.height - 14 * (y_split + 10), 0, 14 * y_split, EXIFWIDTH, bitmap.height - 14 * (y_split + 10));
ctx.drawImage(bitmap, 9 * (x_split + 10), y_split + 10, x_split + (EXIFWIDTH - 10 * x_split), EXIFHEIGHT - 2 * y_split, 9 * x_split, y_split, x_split + (EXIFWIDTH - 10 * x_split), EXIFHEIGHT - 2 * y_split);

//Draw pieces
for (let m = 0; m < shuffleMap.length; m++) {
const piecevalue = parseInt(shuffleMap[m], 16);
ctx.drawImage(
bitmap,
Math.floor((m % 8 + 1) * (x_split + 10)),
Math.floor((Math.floor(m / 8) + 1) * (y_split + 10)),
Math.floor(x_split),
Math.floor(y_split),
Math.floor((piecevalue % 8 + 1) * x_split),
Math.floor((Math.floor(piecevalue / 8) + 1) * y_split),
Math.floor(x_split),
Math.floor(y_split)
);
}
});
}
}
47 changes: 47 additions & 0 deletions web/src/engine/websites/VizShonenJump_e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { TestFixture } from '../../../test/WebsitesFixture';

const configShonenJump = {
plugin: {
id: 'vizshonenjump',
title: 'Viz - Shonen Jump'
},
container: {
url: 'https://www.viz.com/shonenjump/chapters/one-punch-man',
id: '/shonenjump/chapters/one-punch-man',
title: 'One-Punch Man'
},
child: {
id: '/shonenjump/one-punch-man-chapter-1/chapter/4168?action=read',
title: 'Ch. 1'
},
entry: {
index: 4,
size: 942_107,
type: 'image/png'
}
};

new TestFixture(configShonenJump).AssertWebsite();

const configViz = {
plugin: {
id: 'vizshonenjump',
title: 'Viz - Shonen Jump'
},
container: {
url: 'https://www.viz.com/vizmanga/chapters/the-kings-beast',
id: '/vizmanga/chapters/the-kings-beast',
title: 'The King’s Beast'
},
child: {
id: '/vizmanga/the-kings-beast-chapter-1/chapter/35920?action=read',
title: 'Ch. 1'
},
entry: {
index: 1,
size: 730_205,
type: 'image/png'
}
};

new TestFixture(configViz).AssertWebsite();
2 changes: 1 addition & 1 deletion web/src/engine/websites/_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ export { default as VerMangasPorno } from './VerMangasPorno';
export { default as VerManhwa } from './VerManhwa';
export { default as ViewComics } from './ViewComics';
export { default as ViyaFansub } from './ViyaFansub';
export { default as VizShonenJump } from './VizShonenJump';
export { default as VNSharing } from './VNSharing';
export { default as VoidScans } from './VoidScans';
export { default as VortexScans } from './VortexScans';
Expand Down Expand Up @@ -826,7 +827,6 @@ export { default as ToomicsSC } from './legacy/ToomicsSC';
export { default as ToomicsTC } from './legacy/ToomicsTC';
export { default as Toonkor } from './legacy/Toonkor';
export { default as TsundokuTraducoes } from './legacy/TsundokuTraducoes';
export { default as VizShonenJump } from './legacy/VizShonenJump';
export { default as VRVCrunchyroll } from './legacy/VRVCrunchyroll';
export { default as VRVHiDive } from './legacy/VRVHiDive';
export { default as VRVRoosterteeth } from './legacy/VRVRoosterteeth';
Expand Down
Loading