Skip to content

Commit

Permalink
Add Zebrack (#232)
Browse files Browse the repository at this point in the history
* Add Zebrack

protobuf based

* Update Zebrack.ts
  • Loading branch information
MikeZeDev authored Oct 6, 2023
1 parent 892f9d5 commit 7a65610
Show file tree
Hide file tree
Showing 5 changed files with 478 additions and 0 deletions.
347 changes: 347 additions & 0 deletions web/src/engine/websites/Zebrack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
import { Tags } from '../Tags';
import icon from './Zebrack.webp';
import { Chapter, DecoratableMangaScraper, Manga, Page, type MangaPlugin } from '../providers/MangaPlugin';
import * as Common from './decorators/Common';
import { protoTypes } from './Zebrack_proto';
import { FetchProto, FetchRequest, FetchWindowScript } from '../FetchProvider';
import type { Priority } from '../taskpool/TaskPool';

type ZebrackResponse = {
titleDetailView: TitleDetailView,
magazineViewerView: MagazineViewerView,
volumeListView: VolumeListView
}

type VolumeListView = {
volumes: Volume[]
}

type Volume = {
titleId: number,
volumeId: number,
titleName: string,
volumeName: string
}

type TitleDetailView = {
titleId: number,
titleName: string
}

type TitleChapterListViewV3 = {
titleId: number,
groups: ChapterGroupV3[],
titleName: string
}

type ChapterGroupV3 = {
volumeId: number,
chapters: ChapterV3[]
}

type ChapterV3 = {
id: number,
titleId: number,
mainName: string
}

type MagazineViewerView = {
images: ZebrackImage[];
}

type ZebrackImage = {
imageUrl: string,
encryptionKey: string
}

type ChapterViewerViewV3 = {
pages: ChapterPageV3[]
}
type ChapterPageV3 = {
image: ImageV3;
}

type ImageV3 = {
imageUrl: string,
encryptionKey: string
}

type GravureDetailViewV3 = {
gravure: GravureV3
}

type GravureV3 = {
name: string
}

type GravureViewerViewV3 = {
images: ImageV3[]
}

type MagazineDetailViewV3 = {
magazine: MagazineIssue;
}
type MagazineIssue = {
magazineName: string,
issueName: string
}

type VolumeViewerViewV3 = {
pages: VolumePageV3[];
}

type VolumePageV3 = {
image: ImageV3;
}

@Common.MangasNotSupported()

export default class extends DecoratableMangaScraper {

private readonly apiURL = 'https://api.zebrack-comic.com';
private readonly responseRootType = 'Zebrack.Response';

public constructor() {
super('zebrack', 'Zebrack(ゼブラック)', 'https://zebrack-comic.shueisha.co.jp', Tags.Media.Manga, Tags.Language.Japanese, Tags.Source.Official);
}

public override get Icon() {
return icon;
}

public override ValidateMangaURL(url: string): boolean {
return /https?:\/\/zebrack-comic\.shueisha\.co\.jp\/(title|gravure|magazine)\/\d+(\/(issue|volume)\/\d+)?/.test(url);
}

//title : https://zebrack-comic.shueisha.co.jp/title/5123
///gravure : https://zebrack-comic.shueisha.co.jp/gravure/2188
//Magazine : https://zebrack-comic.shueisha.co.jp/magazine/1/issue/14486/detail
//Volume : https://zebrack-comic.shueisha.co.jp/title/46119/volume/178046

public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
const uri = new URL(url);
if (/^\/magazine\//.test(uri.pathname)) {
const magazineId = uri.pathname.match(/\/magazine\/(\d+)/)[1];
const magazineIssueId = uri.pathname.match(/\/issue\/(\d+)/)[1];
const data = await this.fetchMagazineDetail(magazineId, magazineIssueId);
return new Manga(this, provider, uri.pathname, `${data.magazine.magazineName} ${data.magazine.issueName}`);

} else if (/^\/gravure\//.test(uri.pathname)) {
const gravureId = uri.pathname.match(/\/gravure\/(\d+)$/)[1];
const data = await this.fetchGravureDetail(gravureId);
return new Manga(this, provider, uri.pathname, data.gravure.name.trim());
}

const titleId = uri.pathname.match(/\/title\/(\d+)/)[1];
const data = await this.fetchTitleDetail(titleId);
return new Manga(this, provider, uri.pathname, data.titleDetailView.titleName.trim());

}
async fetchMagazineDetail(magazineId: string, magazineIssueId: string): Promise<MagazineDetailViewV3> {
const uri = new URL('/api/v3/magazine_issue_detail', this.apiURL);
uri.searchParams.set('os', 'browser');
uri.searchParams.set('magazine_id', magazineId);
uri.searchParams.set('magazine_issue_id', magazineIssueId);
const request = new FetchRequest(uri.href);
return FetchProto<MagazineDetailViewV3>(request, protoTypes, 'Zebrack.MagazineDetailViewV3');
}

async fetchGravureDetail(gravureId: string): Promise<GravureDetailViewV3> {
const uri = new URL('/api/v3/gravure_detail', this.apiURL);
uri.searchParams.set('os', 'browser');
uri.searchParams.set('gravure_id', gravureId);
const request = new FetchRequest(uri.href);
return FetchProto<GravureDetailViewV3>(request, protoTypes, 'Zebrack.GravureDetailViewV3');
}

async fetchTitleDetail(titleId: string): Promise<ZebrackResponse> {
const uri = new URL('/api/browser/title_detail', this.apiURL);
uri.searchParams.set('os', 'browser');
uri.searchParams.set('title_id', titleId);
const request = new FetchRequest(uri.href);
return FetchProto<ZebrackResponse>(request, protoTypes, this.responseRootType);
}

public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
const parts = manga.Identifier.split('/');
let type = parts[3] || 'chapter';
if (['magazine', 'gravure'].includes(parts[1])) {
type = parts[1];
}
if (type === 'chapter') {
const id = parts[2];
const data = await this.fetchChapterList(id);
const chapters: ChapterV3[] = [];
data.groups.forEach(group => {
chapters.push(...group.chapters);
});
return chapters.map(chapter => new Chapter(this, manga, `chapter/${chapter.titleId}/${chapter.id}`, chapter.mainName));
}

if (type === 'gravure') {
return [new Chapter(this, manga, manga.Identifier.slice(1), manga.Title)];
}

if (type === 'magazine') {
const magazineId = parts[2];
const magazineIssueId = parts[4];
return [new Chapter(this, manga, `magazine/${magazineId}/${magazineIssueId}`, manga.Title)];
}

if (type === 'volume_list' || type === 'volume') {
const id = parts[2];
const data = await this.fetchVolumeList(id);
const volumes = data.volumeListView.volumes;
return volumes.map(volume => new Chapter(this, manga, `volume/${volume.titleId}/${volume.volumeId}`, volume.volumeName));
}
return [];
}

async fetchVolumeList(id: string): Promise<ZebrackResponse> {
const uri = new URL('/api/browser/title_volume_list', this.apiURL);
uri.searchParams.set('os', 'browser');
uri.searchParams.set('title_id', id);
const request = new FetchRequest(uri.href);
return FetchProto<ZebrackResponse>(request, protoTypes, this.responseRootType);
}

async fetchChapterList(id: string): Promise<TitleChapterListViewV3> {
const uri = new URL('/api/v3/title_chapter_list', this.apiURL);
uri.searchParams.set('os', 'browser');
uri.searchParams.set('title_id', id);
const request = new FetchRequest(uri.href);
return FetchProto<TitleChapterListViewV3>(request, protoTypes, 'Zebrack.TitleChapterListViewV3');
}

public override async FetchPages(chapter: Chapter): Promise<Page[]> {
const [type, titleId, chapterId] = chapter.Identifier.split('/');
const request = new FetchRequest(this.URI.href);
const secretKey = await FetchWindowScript<string>(request, 'localStorage.getItem("device_secret_key") || ""');
if (type === 'chapter') {
const data = await this.fetchChapterViewer(titleId, chapterId, secretKey);
if (data.pages) {
return data.pages
.filter(page => page.image && page.image.imageUrl)
.map(page => new Page(this, chapter, new URL(page.image.imageUrl), { encryptionKey: page.image.encryptionKey }));
}
}

if (type === 'gravure') {
const data = await this.fetchGravureViewer(titleId, secretKey);
if (data.images) {
return data.images.map(image => new Page(this, chapter, new URL(image.imageUrl), { encryptionKey: image.encryptionKey }));
}
}

if (type === 'magazine') {
const data = await this.fetchMagazineViewer(titleId, chapterId, secretKey);
if (data.magazineViewerView) {
return data.magazineViewerView.images
.filter(image => image && image.imageUrl)
.map(image => new Page(this, chapter, new URL(image.imageUrl), { encryptionKey: image.encryptionKey }));
}
}

if (type === 'volume') {
const data = await this.fetchVolumeViewer(titleId, chapterId, secretKey);
if (data.pages) {
return data.pages
.filter(page => page.image && page.image.imageUrl)
.map(page => new Page(this, chapter, new URL(page.image.imageUrl), { encryptionKey: page.image.encryptionKey }));
}
}

throw new Error('No image data available, make sure your account is logged in and the chapter is purchased!');
}

async fetchVolumeViewer(titleId: string, volumeId: string, secretKey : string) {
const uri = new URL('/api/v3/manga_volume_viewer', this.apiURL);
uri.searchParams.set('secret', secretKey);
uri.searchParams.set('is_trial', '0');
uri.searchParams.set('os', 'browser');
uri.searchParams.set('title_id', titleId);
uri.searchParams.set('volume_id', volumeId);
let request = new FetchRequest(uri.href);
let data = await FetchProto<VolumeViewerViewV3>(request, protoTypes, 'Zebrack.VolumeViewerViewV3');
if (!data.pages) {
uri.searchParams.set('is_trial', '1');
request = new FetchRequest(uri.href);
data = await FetchProto<VolumeViewerViewV3>(request, protoTypes, 'Zebrack.VolumeViewerViewV3');
}
return data;
}

async fetchMagazineViewer(magazineId: string, magazineIssueId: string, secretKey: string): Promise<ZebrackResponse> {
const uri = new URL('/api/browser/magazine_viewer', this.apiURL);
uri.searchParams.set('secret', secretKey);
uri.searchParams.set('is_trial', '0');
uri.searchParams.set('os', 'browser');
uri.searchParams.set('magazine_id', magazineId);
uri.searchParams.set('magazine_issue_id', magazineIssueId);
let request = new FetchRequest(uri.href);
let data = await FetchProto<ZebrackResponse>(request, protoTypes, this.responseRootType);
if (!data.magazineViewerView) {
uri.searchParams.set('is_trial', '1');
request = new FetchRequest(uri.href);
data = await FetchProto<ZebrackResponse>(request, protoTypes, this.responseRootType);
}
return data;
}

async fetchGravureViewer(gravureId: string, secretKey: string): Promise<GravureViewerViewV3> {
const uri = new URL('/api/v3/gravure_viewer', this.apiURL);
uri.searchParams.set('secret', secretKey);
uri.searchParams.set('is_trial', '0');
uri.searchParams.set('os', 'browser');
uri.searchParams.set('gravure_id', gravureId);
let request = new FetchRequest(uri.href);
let data = await FetchProto<GravureViewerViewV3>(request, protoTypes, 'Zebrack.GravureViewerViewV3');
if (!data.images) {
uri.searchParams.set('is_trial', '1');
request = new FetchRequest(uri.href);
data = await FetchProto<GravureViewerViewV3>(request, protoTypes, 'Zebrack.GravureViewerViewV3');
}
return data;
}

async fetchChapterViewer(titleId: string, chapterId: string, secretKey: string): Promise<ChapterViewerViewV3> {
const uri = new URL('/api/v3/chapter_viewer', this.apiURL);
const params = new URLSearchParams();
params.set('secret', secretKey);
params.set('os', 'browser');
params.set('title_id', titleId);
params.set('chapter_id', chapterId);
params.set('type', 'normal');
const request = new FetchRequest(uri.href, {
method: 'POST',
body: params.toString(),
headers: {
'content-type': 'application/x-www-form-urlencoded'
}
});
return FetchProto<ChapterViewerViewV3>(request, protoTypes, 'Zebrack.ChapterViewerViewV3');
}

public override async FetchImage(page: Page, priority: Priority, signal: AbortSignal): Promise<Blob> {
const data = await Common.FetchImageAjax.call(this, page, priority, signal);
const key: string = page.Parameters ? page.Parameters['encryptionKey'] as string : undefined;
if (!key) return data;
const encrypted = await new Response(data).arrayBuffer();
const decrypted = XORDecrypt(new Uint8Array(encrypted), key);
return new Blob([decrypted], { type: data.type });
}

}

function XORDecrypt(encrypted: Uint8Array, key: string) {
if (key) {
const t = new Uint8Array(key.match(/.{1,2}/g).map(e => parseInt(e, 16)));
const s = new Uint8Array(encrypted);
for (let n = 0; n < s.length; n++) {
s[n] ^= t[n % t.length];
}
return s;
} else {
return encrypted;
}
}
Binary file added web/src/engine/websites/Zebrack.webp
Binary file not shown.
25 changes: 25 additions & 0 deletions web/src/engine/websites/Zebrack_e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { TestFixture, type Config } from '../../../test/WebsitesFixture';

const config: Config = {
plugin: {
id: 'zebrack',
title: 'Zebrack(ゼブラック)'
},
container: {
url: 'https://zebrack-comic.shueisha.co.jp/magazine/1/issue/14486/detail',
id: '/magazine/1/issue/14486/detail',
title: '週刊少年ジャンプ 2023年44号'
},
child: {
id: 'magazine/1/14486',
title: '週刊少年ジャンプ 2023年44号'
},
entry: {
index: 0,
size: 485_738,
type: 'image/jpeg'
}
};

const fixture = new TestFixture(config);
describe(fixture.Name, () => fixture.AssertWebsite());
Loading

0 comments on commit 7a65610

Please sign in to comment.