Skip to content

Commit

Permalink
Delitoon : move code to template DelitoonBase (#860)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeZeDev authored Nov 4, 2024
1 parent d7fb54e commit 648d016
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 153 deletions.
4 changes: 2 additions & 2 deletions web/src/engine/websites/Bomtoon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Tags } from '../Tags';
import { FetchJSON } from '../platform/FetchProvider';
import {type MangaPlugin, Manga, type Chapter, Page } from '../providers/MangaPlugin';
import icon from './Bomtoon.webp';
import Delitoon, { type APIManga, type APIResult } from './Delitoon';
import * as Common from './decorators/Common';
import { WebsiteResourceKey as R } from '../../i18n/ILocale';
import type { Priority } from '../taskpool/DeferredTask';
import DeScramble from '../transformers/ImageDescrambler';
import { DelitoonBase, type APIManga, type APIResult } from './templates/DelitoonBase';

type APIMangas = {
content: APIManga[]
Expand All @@ -28,7 +28,7 @@ type ScrambleParams = {
defaultHeight: number,
}

export default class extends Delitoon {
export default class extends DelitoonBase {

public constructor() {
super('bomtoon', `Bomtoon`, 'https://www.bomtoon.com', [Tags.Language.Korean, Tags.Media.Manhwa, Tags.Source.Official]);
Expand Down
4 changes: 2 additions & 2 deletions web/src/engine/websites/BomtoonCN.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tags } from '../Tags';
import icon from './Bomtoon.webp';
import Delitoon from './Delitoon';
export default class extends Delitoon {
import { DelitoonBase } from './templates/DelitoonBase';
export default class extends DelitoonBase {
public constructor() {
super('bomtooncn', `Bomtoon (Chinese)`, 'https://www.bomtoon.tw', [Tags.Language.Chinese, Tags.Media.Manhwa, Tags.Source.Official]);
this.BalconyID = 'BOMTOON_TW';
Expand Down
4 changes: 2 additions & 2 deletions web/src/engine/websites/Bontoon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tags } from '../Tags';
import icon from './Bontoon.webp';
import Delitoon from './Delitoon';
export default class extends Delitoon {
import { DelitoonBase } from './templates/DelitoonBase';
export default class extends DelitoonBase {
public constructor() {
super('bontoon', `Bontoon`, 'https://www.bontoon.com', [Tags.Language.French, Tags.Media.Manhwa, Tags.Rating.Pornographic, Tags.Source.Official]);
this.BalconyID = 'BONTOON_COM';
Expand Down
4 changes: 2 additions & 2 deletions web/src/engine/websites/Boomtoon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tags } from '../Tags';
import icon from './BoomToon.webp';
import Delitoon from './Delitoon';
export default class extends Delitoon {
import { DelitoonBase } from './templates/DelitoonBase';
export default class extends DelitoonBase {
public constructor() {
super('boomtoon', `Boomtoon`, 'https://www.boomtoon.com', [Tags.Language.Thai, Tags.Media.Manhwa, Tags.Source.Official]);
this.BalconyID = 'BOOMTOON_COM';
Expand Down
143 changes: 4 additions & 139 deletions web/src/engine/websites/Delitoon.ts
Original file line number Diff line number Diff line change
@@ -1,149 +1,14 @@
import { Tags } from '../Tags';
import icon from './Delitoon.webp';
import { Chapter, DecoratableMangaScraper, Manga, Page, type MangaPlugin } from '../providers/MangaPlugin';
import * as Common from './decorators/Common';
import { FetchJSON } from '../platform/FetchProvider';
import { Exception } from '../Error';
import { WebsiteResourceKey as R } from '../../i18n/ILocale';
import { DelitoonBase } from './templates/DelitoonBase';

export type APIResult<T> = {
result: string,
error?: {
code: string
}
data: T
}

export type APIManga = {
id: number,
alias: string,
title: string
episodes: APIChapter[]
}

type APIChapter = {
id: string,
alias: string,
title: string,
subTitle: string
}

export type APIPages = {
images: {
imagePath: string
}[]
}

type APIUser = {
user?: {
accessToken: APIToken,
},
};

type APIToken = {
token: string,
expiredAt: number,
};

@Common.ImageAjax(true)
export default class extends DecoratableMangaScraper {
private readonly Platform: string = 'WEB';
private authorization: APIToken = undefined;
protected readonly apiUrl = new URL('/api/balcony-api-v2/', this.URI);
protected BalconyID: string = 'DELITOON_COM';

public constructor(id = 'delitoon', label = 'Delitoon', url = 'https://www.delitoon.com', tags = [Tags.Media.Manhwa, Tags.Language.French, Tags.Source.Official]) {
super(id, label, url, ...tags);
export default class extends DelitoonBase {
public constructor() {
super('delitoon', 'Delitoon', 'https://www.delitoon.com', [Tags.Media.Manhwa, Tags.Language.French, Tags.Source.Official]);

}

public override get Icon() {
return icon;
}

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

public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
const mangaid = new URL(url).href.match(/\/detail\/([^/]+)/)[1];
const endpointUrl = new URL(`contents/${mangaid}`, this.apiUrl);
endpointUrl.searchParams.set('isNotLoginAdult', 'true');
const { data } = await FetchJSON<APIResult<APIManga>>(this.CreateRequest(endpointUrl));
return new Manga(this, provider, mangaid, data.title.trim());
}

public override async FetchMangas(provider: MangaPlugin): Promise<Manga[]> {
const url = new URL('contents/search', this.apiUrl);
const promises = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(character => {
url.search = new URLSearchParams({
searchText: character,
isCheckDevice: 'true',
isIncludeAdult: 'true',
contentsThumbnailType: 'MAIN'
}).toString();

return FetchJSON<APIResult<APIManga[]>>(this.CreateRequest(url));
});

const results = (await Promise.all(promises)).reduce((accumulator: Manga[], element) => {
const mangas = element.data.map(element => new Manga(this, provider, element.alias, element.title.trim()));
accumulator.push(...mangas);
return accumulator;
}, []);

return results.distinct();
}

public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
await this.UpdateToken();
const url = new URL(`contents/${manga.Identifier}`, this.apiUrl);
url.searchParams.set('isNotLoginAdult', 'true');
const { data } = await FetchJSON<APIResult<APIManga>>(this.CreateRequest(url));
return data.episodes.map(element => {
let title = element.title.trim();
title += element.subTitle ? ' : ' + element.subTitle.trim() : '';
return new Chapter(this, manga, element.alias, title);
});
}

public override async FetchPages(chapter: Chapter): Promise<Page[]> {
await this.UpdateToken();
const url = new URL(`contents/viewer/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.apiUrl);
url.searchParams.set('isNotLoginAdult', 'true');
const { result, error, data } = await FetchJSON<APIResult<APIPages>>(this.CreateRequest(url));
if (result == 'ERROR') {
switch (error.code) {
case 'NOT_LOGIN_USER':
case 'UNAUTHORIZED_CONTENTS':
throw new Exception(R.Plugin_Common_Chapter_UnavailableError);
}
}
return data.images.map(element => new Page(this, chapter, new URL(element.imagePath)));
}

protected CreateRequest(url: URL, includeAuthorization = true, body: string = undefined): Request {
const request = new Request(url, {
method: body? 'POST':'GET',
headers: {
'Referer': this.URI.origin,
'X-Balcony-Id': this.BalconyID,
'X-Platform': this.Platform,
'Content-type': body ? 'application/json' : undefined
},
body: body
});
if (this.authorization && includeAuthorization) {
request.headers.set('authorization', ' Bearer ' + this.authorization.token);
}
return request;
}

protected async UpdateToken() {
if (!this.authorization || this.authorization.expiredAt < Date.now()) {
const url = new URL('/api/auth/session', this.URI);
const { user } = await FetchJSON<APIUser>(this.CreateRequest(url, false));
this.authorization = user?.accessToken;
}
}
}
4 changes: 2 additions & 2 deletions web/src/engine/websites/DelitoonDE.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tags } from '../Tags';
import icon from './DelitoonDE.webp';
import Delitoon from './Delitoon';
export default class extends Delitoon {
import { DelitoonBase } from './templates/DelitoonBase';
export default class extends DelitoonBase {
public constructor() {
super('delitoonde', `Delitoon (German)`, 'https://www.delitoon.de', [Tags.Language.German, Tags.Media.Manhwa, Tags.Source.Official]);
this.BalconyID = 'DELITOON_DE';
Expand Down
4 changes: 2 additions & 2 deletions web/src/engine/websites/DelitoonX.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tags } from '../Tags';
import icon from './DelitoonX.webp';
import Delitoon from './Delitoon';
export default class extends Delitoon {
import { DelitoonBase } from './templates/DelitoonBase';
export default class extends DelitoonBase {
public constructor() {
super('delitoonx', `DelitoonX`, 'https://www.delitoonx.com', [Tags.Language.French, Tags.Media.Manhwa, Tags.Rating.Pornographic, Tags.Source.Official]);
this.BalconyID = 'DELITOONX_COM';
Expand Down
4 changes: 2 additions & 2 deletions web/src/engine/websites/LezhinES.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tags } from '../Tags';
import icon from './LezhinES.webp';
import Delitoon from './Delitoon';
export default class extends Delitoon {
import { DelitoonBase } from './templates/DelitoonBase';
export default class extends DelitoonBase {
public constructor() {
super('lezhin-es', `Lezhin (Spanish)`, 'https://www.lezhin.es', [Tags.Language.Spanish, Tags.Media.Manhwa, Tags.Source.Official]);
this.BalconyID = 'LEZHIN_ES';
Expand Down
144 changes: 144 additions & 0 deletions web/src/engine/websites/templates/DelitoonBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Chapter, DecoratableMangaScraper, Manga, Page, type MangaPlugin } from '../../providers/MangaPlugin';
import * as Common from '../decorators/Common';
import { FetchJSON } from '../../platform/FetchProvider';
import { Exception } from '../../Error';
import { WebsiteResourceKey as R } from '../../../i18n/ILocale';
import type { Tag } from '../../Tags';

export type APIResult<T> = {
result: string,
error?: {
code: string
}
data: T
}

export type APIManga = {deli

id: number,
alias: string,
title: string
episodes: APIChapter[]
}

type APIChapter = {
id: string,
alias: string,
title: string,
subTitle: string
}

export type APIPages = {
images: {
imagePath: string
}[]
}

type APIUser = {
user?: {
accessToken: APIToken,
},
};

type APIToken = {
token: string,
expiredAt: number,
};

@Common.ImageAjax(true)
export class DelitoonBase extends DecoratableMangaScraper {
private readonly Platform: string = 'WEB';
private authorization: APIToken = undefined;
protected readonly apiUrl = new URL('/api/balcony-api-v2/', this.URI);
protected BalconyID: string = 'DELITOON_COM';

public constructor(id: string, label: string, url: string, tags: Tag[]) {
super(id, label, url, ...tags);
}

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

public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
const mangaid = new URL(url).href.match(/\/detail\/([^/]+)/)[1];
const endpointUrl = new URL(`contents/${mangaid}`, this.apiUrl);
endpointUrl.searchParams.set('isNotLoginAdult', 'true');
const { data } = await FetchJSON<APIResult<APIManga>>(this.CreateRequest(endpointUrl));
return new Manga(this, provider, mangaid, data.title.trim());
}

public override async FetchMangas(provider: MangaPlugin): Promise<Manga[]> {
const url = new URL('contents/search', this.apiUrl);
const promises = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(character => {
url.search = new URLSearchParams({
searchText: character,
isCheckDevice: 'true',
isIncludeAdult: 'true',
contentsThumbnailType: 'MAIN'
}).toString();

return FetchJSON<APIResult<APIManga[]>>(this.CreateRequest(url));
});

const results = (await Promise.all(promises)).reduce((accumulator: Manga[], element) => {
const mangas = element.data.map(element => new Manga(this, provider, element.alias, element.title.trim()));
accumulator.push(...mangas);
return accumulator;
}, []);

return results.distinct();
}

public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
await this.UpdateToken();
const url = new URL(`contents/${manga.Identifier}`, this.apiUrl);
url.searchParams.set('isNotLoginAdult', 'true');
const { data } = await FetchJSON<APIResult<APIManga>>(this.CreateRequest(url));
return data.episodes.map(element => {
let title = element.title.trim();
title += element.subTitle ? ' : ' + element.subTitle.trim() : '';
return new Chapter(this, manga, element.alias, title);
});
}

public override async FetchPages(chapter: Chapter): Promise<Page[]> {
await this.UpdateToken();
const url = new URL(`contents/viewer/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.apiUrl);
url.searchParams.set('isNotLoginAdult', 'true');
const { result, error, data } = await FetchJSON<APIResult<APIPages>>(this.CreateRequest(url));
if (result == 'ERROR') {
switch (error.code) {
case 'NOT_LOGIN_USER':
case 'UNAUTHORIZED_CONTENTS':
throw new Exception(R.Plugin_Common_Chapter_UnavailableError);
}
}
return data.images.map(element => new Page(this, chapter, new URL(element.imagePath)));
}

protected CreateRequest(url: URL, includeAuthorization = true, body: string = undefined): Request {
const request = new Request(url, {
method: body ? 'POST' : 'GET',
headers: {
'Referer': this.URI.origin,
'X-Balcony-Id': this.BalconyID,
'X-Platform': this.Platform,
'Content-type': body ? 'application/json' : undefined
},
body: body
});
if (this.authorization && includeAuthorization) {
request.headers.set('authorization', ' Bearer ' + this.authorization.token);
}
return request;
}

protected async UpdateToken() {
if (!this.authorization || this.authorization.expiredAt < Date.now()) {
const url = new URL('/api/auth/session', this.URI);
const { user } = await FetchJSON<APIUser>(this.CreateRequest(url, false));
this.authorization = user?.accessToken;
}
}
}
8 changes: 8 additions & 0 deletions web/src/engine/websites/templates/DelitoonBase_e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../Bomtoon_e2e';
import '../BomtoonCN_e2e';
import '../Bontoon_e2e';
import '../BoomToon_e2e';
import '../Delitoon_e2e';
import '../DelitoonDE_e2e';
import '../DelitoonX_e2e';
import '../LezhinES_e2e';

0 comments on commit 648d016

Please sign in to comment.