diff --git a/web/src/engine/websites/BookHodai.ts b/web/src/engine/websites/BookHodai.ts new file mode 100644 index 0000000000..b22a08b77f --- /dev/null +++ b/web/src/engine/websites/BookHodai.ts @@ -0,0 +1,55 @@ +import { Tags } from '../Tags'; +import icon from './BookHodai.webp'; +import { Chapter, DecoratableMangaScraper, type Manga, type MangaPlugin } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchHTML } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaLabelExtractor(element: HTMLElement): string { + return element.textContent.split('>').pop().trim() || element.textContent.trim(); +} + +@Common.MangaCSS(/^{origin}\/[^/]+\/backnumber\/\d+$/, 'ol.c-breadcrumb li:last-of-type a, div.p-book-overview__detail h2.p-book-overview__detail-bookname', MangaLabelExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('bookhodai', `BookHodai`, 'https://bookhodai.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override async FetchMangas(provider: MangaPlugin): Promise { + const paths = ['magazine', 'magazine_pop', 'manga', 'manga_pop']; + const mangaList: Manga[] = []; + for (const path of paths) { + const mangas = await Common.FetchMangasMultiPageCSS.call(this, provider, `/search/${path}?page={page}`, '.p-bookdetail-contents__title a'); + mangaList.push(...mangas); + } + return mangaList.distinct(); + } + + public override async FetchChapters(manga: Manga): Promise { + const chapters: Chapter[] = []; + const dom = await FetchHTML(new Request(new URL(manga.Identifier, this.URI))); + + //get first "chapter" (book) details + const bookdetails = dom.querySelector('section.p-book-overview'); + let title = (bookdetails.querySelector('span.p-book-overview__detail-volnumber') ?? bookdetails.querySelector('h2.p-book-overview__detail-vol')).textContent.replaceAll('\n', '').trim(); + title = title.replace(manga.Title, '').trim() != '' ? title.replace(manga.Title, '').trim() : title; + const chapterlinkNode = bookdetails.querySelector('a[href*="viewer"'); + if (chapterlinkNode) chapters.push(new Chapter(this, manga, chapterlinkNode.pathname + chapterlinkNode.search, title)); + + const chaptersNodes = [...dom.querySelectorAll('div.p-book-backnumber-series__item')]; + for (const chapter of chaptersNodes) { + const title = (chapter.querySelector('.p-book-backnumber-series__volnumber') ?? chapter.querySelector('.p-book-backnumber-series__book-nm')).textContent.trim(); + const link = chapter.querySelector('a[href*="viewer"]'); + chapters.push(new Chapter(this, manga, link.pathname + link.search, title.replace(manga.Title, '').trim())); + } + return chapters.distinct(); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/BookHodai.webp b/web/src/engine/websites/BookHodai.webp new file mode 100644 index 0000000000..8ee8ef8694 Binary files /dev/null and b/web/src/engine/websites/BookHodai.webp differ diff --git a/web/src/engine/websites/BookHodai_e2e.ts b/web/src/engine/websites/BookHodai_e2e.ts new file mode 100644 index 0000000000..cc23a18850 --- /dev/null +++ b/web/src/engine/websites/BookHodai_e2e.ts @@ -0,0 +1,52 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +new TestFixture({ + plugin: { + id: 'bookhodai', + title: 'BookHodai' + }, + container: { + url: 'https://bookhodai.jp/magazine/backnumber/1029551', + id: '/magazine/backnumber/1029551', + title: 'comicグラスト', + timeout: 15000 + + }, + child: { + id: '/viewer?book_id=2000035407&branch_no=01&book_type=3', + title: 'vol.85', + timeout: 20000 + }, + entry: { + index: 0, + size: 3_984_399, + type: 'image/png', + timeout: 30000 + + } +}).AssertWebsite(); + +new TestFixture({ + plugin: { + id: 'bookhodai', + title: 'BookHodai' + }, + container: { + url: 'https://bookhodai.jp/manga/backnumber/62789', + id: '/manga/backnumber/62789', + title: '風光る', + timeout: 15000 + + }, + child: { + id: '/viewer?book_id=3000099022&branch_no=01&book_type=4', + title: '1 ~ 44', + timeout: 20000 + }, + entry: { + index: 0, + size: 4_011_228, + type: 'image/png', + timeout: 30000 + } +}).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/BookLive.ts b/web/src/engine/websites/BookLive.ts new file mode 100644 index 0000000000..9719ddd1d4 --- /dev/null +++ b/web/src/engine/websites/BookLive.ts @@ -0,0 +1,29 @@ +import { Tags } from '../Tags'; +import icon from './BookLive.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; +function ChapterExtractor(anchor: HTMLAnchorElement) { + return { + id: '/bviewer/s/?cid=' + anchor.dataset.title + '_' + anchor.dataset.vol, + title: anchor.closest('.series_list_detail').querySelector('a[class*=sl-title]').text.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/product\/index\/title_id\/\d+\/vol_no\/\d+$/, 'li.contents span.book_title') +@Common.MangasNotSupported() +@Common.ChaptersSinglePageCSS('div#slide_up_top li.item div.buttons a.bl-bviewer[data-title][data-vol]', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130) +@SpeedBinb.ImageAjax() + +export default class extends DecoratableMangaScraper { + + public constructor() { + super('booklive', `BookLive`, 'https://booklive.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/BookLive.webp b/web/src/engine/websites/BookLive.webp similarity index 100% rename from web/src/engine/websites/legacy/BookLive.webp rename to web/src/engine/websites/BookLive.webp diff --git a/web/src/engine/websites/BookLive_e2e.ts b/web/src/engine/websites/BookLive_e2e.ts new file mode 100644 index 0000000000..0f6e9b7305 --- /dev/null +++ b/web/src/engine/websites/BookLive_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'booklive', + title: 'BookLive' + }, + container: { + url: 'https://booklive.jp/product/index/title_id/20063601/vol_no/001', + id: '/product/index/title_id/20063601/vol_no/001', + title: '火の神さまの掃除人ですが、いつの間にか花嫁として溺愛されています【単話】' + }, + child: { + id: '/bviewer/s/?cid=20063601_001', + title: '1' + }, + entry: { + index: 0, + size: 2_219_447, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/Cmoa.ts b/web/src/engine/websites/Cmoa.ts new file mode 100644 index 0000000000..1bdb17715e --- /dev/null +++ b/web/src/engine/websites/Cmoa.ts @@ -0,0 +1,52 @@ +import { Tags } from '../Tags'; +import icon from './Cmoa.webp'; +import { Chapter, DecoratableMangaScraper, type MangaPlugin, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +@Common.MangasNotSupported() +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016452) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('cmoa', `コミックシーモア (Cmoa)`, 'https://www.cmoa.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + + public override ValidateMangaURL(url: string): boolean { + return /https:\/\/www\.cmoa\.jp\/title\/\d+\/(vol\/\d+\/)?$/.test(url); + } + + public override async FetchManga(provider: MangaPlugin, url: string): Promise { + const newUrl = url.replace(/\/vol\/\d+\/$/, '/'); + return await Common.FetchMangaCSS.call(this, provider, newUrl, '#GA_this_page_title_name'); + } + + public override async FetchChapters(manga: Manga): Promise { + const pages = await FetchCSS(new Request(new URL(manga.Identifier, this.URI).href), '#comic_list > .pagination:nth-child(1) li:nth-last-child(2) a'); + const chapters = []; + const totalPage = pages.length == 0 ? 1 : parseInt(new URL(pages[0].href).searchParams.get('page')); + for (let i = 0; i < totalPage; i++) { + const uri = new URL(manga.Identifier, this.URI); + uri.searchParams.set('page', String(i + 1)); + const pageRequest = new Request(uri); + const data = await FetchCSS(pageRequest, '.title_vol_vox_vols .title_vol_vox_vols_i'); + for (const element of data) { + const chapterLink = element.querySelector('a[href^="/reader/"]'); + if (!chapterLink) { + continue; + } + const chapterUrl = new URL(chapterLink.href, this.URI); + const id = chapterUrl.searchParams.get('content_id'); + const u0 = chapterUrl.pathname.startsWith('/reader/sample') ? 1 : 0; + const title = element.querySelector('.title_details_title_name_h2').textContent.trim(); + chapters.push(new Chapter(this, manga, `/bib/speedreader/?cid=${id.slice(1, 11)}_jp_${id.slice(11, 15)}&u0=${u0}&u1=0`, title.replace('NEW\n', '').trim())); + } + } + return chapters; + } +} \ No newline at end of file diff --git a/web/src/engine/websites/Cmoa.webp b/web/src/engine/websites/Cmoa.webp new file mode 100644 index 0000000000..26b8475537 Binary files /dev/null and b/web/src/engine/websites/Cmoa.webp differ diff --git a/web/src/engine/websites/Cmoa_e2e.ts b/web/src/engine/websites/Cmoa_e2e.ts new file mode 100644 index 0000000000..a5db576f54 --- /dev/null +++ b/web/src/engine/websites/Cmoa_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'cmoa', + title: 'コミックシーモア (Cmoa)' + }, + container: { + url: 'https://www.cmoa.jp/title/151961/vol/24/', + id: '/title/151961/', + title: '呪術廻戦' + }, + child: { + id: '/bib/speedreader/?cid=0000151961_jp_0021&u0=1&u1=0', + title: '呪術廻戦 21' + }, + entry: { + index: 0, + size: 2_880_733, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/ComicBrise.ts b/web/src/engine/websites/ComicBrise.ts new file mode 100644 index 0000000000..ecabdafd4b --- /dev/null +++ b/web/src/engine/websites/ComicBrise.ts @@ -0,0 +1,28 @@ +import { Tags } from '../Tags'; +import icon from './ComicBrise.webp'; +import { Chapter, DecoratableMangaScraper, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +@Common.MangaCSS(/^{origin}\/contents\/[^/]+\/$/, '.post-title') +@Common.MangasSinglePagesCSS(['/titlelist'], '.list-works a') +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('comicbrise', `Comic-Brise`, 'https://www.comic-brise.com', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override async FetchChapters(manga: Manga): Promise { + return (await FetchCSS(new Request(new URL(manga.Identifier, this.URI)), '.modal.modal-chapter .modal-body')) + .reverse() + //.filter(e => e.querySelector(".banner-trial img").getAttribute("alt") == "FREE") //dont filter for free chapter + .map(element => new Chapter(this, manga, element.querySelector('.banner-trial a').pathname, element.querySelector('.primary-title').textContent.trim())); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicBrise.webp b/web/src/engine/websites/ComicBrise.webp similarity index 100% rename from web/src/engine/websites/legacy/ComicBrise.webp rename to web/src/engine/websites/ComicBrise.webp diff --git a/web/src/engine/websites/ComicBrise_e2e.ts b/web/src/engine/websites/ComicBrise_e2e.ts new file mode 100644 index 0000000000..8326c107bb --- /dev/null +++ b/web/src/engine/websites/ComicBrise_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'comicbrise', + title: 'Comic-Brise' + }, + container: { + url: 'https://www.comic-brise.com/contents/mobu/', + id: '/contents/mobu/', + title: 'モブ顔令嬢~乙女ゲー世界の悪役令嬢に転生したのにどうしてこうなった~' + }, + child: { + id: '/comic_ep/mobu_ep1', + title: '第1話' + }, + entry: { + index: 0, + size: 4_607_990, + type: 'image/png', + timeout: 10000 + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/ComicMeteor.ts b/web/src/engine/websites/ComicMeteor.ts new file mode 100644 index 0000000000..06df66a412 --- /dev/null +++ b/web/src/engine/websites/ComicMeteor.ts @@ -0,0 +1,40 @@ +import { Tags } from '../Tags'; +import icon from './ComicMeteor.webp'; +import { Chapter, DecoratableMangaScraper, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.querySelector('img').getAttribute('alt').trim() + }; +} + +@Common.MangaCSS(/^{origin}\/[^/]+\/$/, 'div.h2ttl_other') +@Common.MangasMultiPageCSS('/wp-admin/admin-ajax.php?action=get_flex_titles_for_toppage&get_num=64&page={page}', 'div.update_work_size div.update_work_info_img a', 1, 1, 0, MangaExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('comicmeteor', `COMICメテオ (COMIC Meteor)`, 'https://comic-meteor.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + + public override async FetchChapters(manga: Manga): Promise { + const [ data ] = await FetchCSS(new Request(new URL(manga.Identifier, this.URI)), 'div#contents'); + + let chapterList = [...data.querySelectorAll('div.work_episode div.work_episode_box div.work_episode_table div.work_episode_link_btn.work_episode_link_orange a')] + .map(element => new Chapter(this, manga, element.pathname, element.closest('div.work_episode_table').querySelector('div.work_episode_txt').innerText.replace(manga.Title, '').trim())); + + if (chapterList.length == 0) { + chapterList = [...data.querySelectorAll('div.latest_info_box div.latest_info_link_btn01 a')] + .map(element => new Chapter(this, manga, element.pathname, element.text.replace('読む', '').trim())); + } + return chapterList; + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicMeteor.webp b/web/src/engine/websites/ComicMeteor.webp similarity index 100% rename from web/src/engine/websites/legacy/ComicMeteor.webp rename to web/src/engine/websites/ComicMeteor.webp diff --git a/web/src/engine/websites/ComicMeteor_e2e.ts b/web/src/engine/websites/ComicMeteor_e2e.ts new file mode 100644 index 0000000000..1eda5d0f46 --- /dev/null +++ b/web/src/engine/websites/ComicMeteor_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'comicmeteor', + title: 'COMICメテオ (COMIC Meteor)' + }, + container: { + url: 'https://comic-meteor.jp/isekaiseihukuki/', + id: '/isekaiseihukuki/', + title: '異世界征服記~不遇種族たちの最強国家~' + }, + child: { + id: '/ptdata/isekaiseihukuki/0001/', + title: '第1話' + }, + entry: { + index: 0, + size: 3_860_271, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/ComicPolaris.ts b/web/src/engine/websites/ComicPolaris.ts new file mode 100644 index 0000000000..364ac8d0f5 --- /dev/null +++ b/web/src/engine/websites/ComicPolaris.ts @@ -0,0 +1,41 @@ +import { Tags } from '../Tags'; +import icon from './ComicPolaris.webp'; +import { Chapter, DecoratableMangaScraper, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.querySelector('img').getAttribute('alt').trim() + }; +} + +@Common.MangaCSS(/^{origin}\/[^/]+\/$/, 'div#contents div.h2_area_comic h2.h2ttl_comic') +@Common.MangasMultiPageCSS('/wp-admin/admin-ajax.php?action=get_flex_titles_for_toppage&get_num=64&page={page}', 'div.update_work_size div.update_work_info_img a', 1, 1, 0, MangaExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('comicpolaris', `COMICポラリス (COMIC Polaris)`, 'https://comic-polaris.jp', Tags.Media.Manga, Tags.Language.Japanese, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + + public override async FetchChapters(manga: Manga): Promise { + const [ data ] = await FetchCSS(new Request(new URL(manga.Identifier, this.URI)), 'div#contents'); + + let chapterList = [...data.querySelectorAll('div.work_episode div.work_episode_box div.work_episode_table div.work_episode_link_btn a')] + .map(element => new Chapter(this, manga, element.pathname, element.closest('div.work_episode_table').querySelector('div.work_episode_txt').innerText.replace(manga.Title, '').trim())); + + if (chapterList.length == 0) { + chapterList = [...data.querySelectorAll('div.latest_info_box div.latest_info_link_btn01 a')] + .map(element => new Chapter(this, manga, element.pathname, element.text.replace('読む', '').trim())); + } + return chapterList; + } + +} \ No newline at end of file diff --git a/web/src/engine/websites/ComicPolaris.webp b/web/src/engine/websites/ComicPolaris.webp new file mode 100644 index 0000000000..a18789f956 Binary files /dev/null and b/web/src/engine/websites/ComicPolaris.webp differ diff --git a/web/src/engine/websites/ComicPolaris_e2e.ts b/web/src/engine/websites/ComicPolaris_e2e.ts new file mode 100644 index 0000000000..1bd425c9cc --- /dev/null +++ b/web/src/engine/websites/ComicPolaris_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'comicpolaris', + title: 'COMICポラリス (COMIC Polaris)' + }, + container: { + url: 'https://comic-polaris.jp/ekidemita/', + id: '/ekidemita/', + title: '今日、駅で見た可愛い女の子。' + }, + child: { + id: '/ptdata/ekidemita/0001/', + title: '第1話' + }, + entry: { + index: 0, + size: 1_869_995, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/ComicPorta.ts b/web/src/engine/websites/ComicPorta.ts new file mode 100644 index 0000000000..c3f6b7f19b --- /dev/null +++ b/web/src/engine/websites/ComicPorta.ts @@ -0,0 +1,29 @@ +import { Tags } from '../Tags'; +import icon from './ComicPorta.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function ChapterExtractor(element: HTMLElement) { + return { + id: element.querySelector('a').pathname, + title: element.parentNode.querySelector('p.title').textContent.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/series\/\d+\/$/, 'div#breadcrumb li:last-of-type') +@Common.MangasSinglePagesCSS(['/series/'], 'div.series-list ul li h3.title a') +@Common.ChaptersSinglePageCSS('ul.episode-list li.episode div.inner div.wrap p.episode-btn', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('comicporta', `COMICポルタ (Comic Porta)`, 'https://comic-porta.com', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } +} diff --git a/web/src/engine/websites/ComicPorta.webp b/web/src/engine/websites/ComicPorta.webp new file mode 100644 index 0000000000..93de513dd0 Binary files /dev/null and b/web/src/engine/websites/ComicPorta.webp differ diff --git a/web/src/engine/websites/ComicPorta_e2e.ts b/web/src/engine/websites/ComicPorta_e2e.ts new file mode 100644 index 0000000000..fbf2014cf3 --- /dev/null +++ b/web/src/engine/websites/ComicPorta_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'comicporta', + title: 'COMICポルタ (Comic Porta)' + }, + container: { + url: 'https://comic-porta.com/series/124/', + id: '/series/124/', + title: 'モスのいる日常' + }, + child: { + id: '/p_data/moth001/', + title: '1話「モスとの出会い」' + }, + entry: { + index: 0, + size: 1_320_774, + type: 'image/png', + timeout: 20000 + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/ComicValkyrie.ts b/web/src/engine/websites/ComicValkyrie.ts new file mode 100644 index 0000000000..774c3d832e --- /dev/null +++ b/web/src/engine/websites/ComicValkyrie.ts @@ -0,0 +1,44 @@ +import { Tags } from '../Tags'; +import icon from './ComicValkyrie.webp'; +import { DecoratableMangaScraper, Manga, type MangaPlugin } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(element: HTMLElement) { + return { + id: new URL(element.querySelector('a').href).pathname.replace('/new.html', '/'), + title: element.querySelector('.title').textContent.replace(/\s*THE COMIC\s*/i, '').trim() + }; +} +function ChapterExtractor(element: HTMLElement) { + return { + id: element.parentElement.querySelector('a.read_bt').pathname, + title: element.textContent.trim() + }; +} + +@Common.MangasSinglePagesCSS(['/list'], '.box_wrap .box', MangaExtractor) +@Common.ChaptersSinglePageCSS('#new_story .title, #back_number .title', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('comicvalkyrie', `Comic Valkyrie`, 'https://www.comic-valkyrie.com', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + + public override ValidateMangaURL(url: string): boolean { + return new RegExp(`^${this.URI.origin}/[^/]+(/|/new.html)?$`).test(url); + } + + public override async FetchManga(provider: MangaPlugin, url: string): Promise { + const data = await FetchCSS(new Request(url), 'meta[property = "og:title"]'); + const id = new URL(url).pathname.replace('/new.html', '/'); + const title = data[0].content.replace(/\s*THE COMIC\s*/i, '').trim(); + return new Manga(this, provider, id, title); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicValkyrie.webp b/web/src/engine/websites/ComicValkyrie.webp similarity index 100% rename from web/src/engine/websites/legacy/ComicValkyrie.webp rename to web/src/engine/websites/ComicValkyrie.webp diff --git a/web/src/engine/websites/ComicValkyrie_e2e.ts b/web/src/engine/websites/ComicValkyrie_e2e.ts new file mode 100644 index 0000000000..64da6c3a1a --- /dev/null +++ b/web/src/engine/websites/ComicValkyrie_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'comicvalkyrie', + title: 'Comic Valkyrie' + }, + container: { + url: 'https://www.comic-valkyrie.com/teisou/new.html', //testing the removing of new.html on purpose + id: '/teisou/', + title: '貞操逆転世界' + }, + child: { + id: '/samplebook/val_teisou01/', + title: '第1話' + }, + entry: { + index: 0, + size: 1_249_355, + type: 'image/png', + timeout: 20000 + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/DigitalMargaret.ts b/web/src/engine/websites/DigitalMargaret.ts new file mode 100644 index 0000000000..d77f1d4978 --- /dev/null +++ b/web/src/engine/websites/DigitalMargaret.ts @@ -0,0 +1,35 @@ +import { Tags } from '../Tags'; +import icon from './DigitalMargaret.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.querySelector('img').getAttribute('alt').trim() + }; +} +function ChapterExtractor(element: HTMLElement) { + return { + id: element.querySelector('a').pathname, + title: element.querySelector('p').textContent.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/detail\/[^/]+\/$/, 'section#product div.content h3') +@Common.MangasSinglePagesCSS(['/'], 'section#serial ul.serial-list li a', MangaExtractor) +@Common.ChaptersSinglePageCSS('section#product div.list div.box div.number', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() + +export default class extends DecoratableMangaScraper { + public constructor() { + super('digitalmargaret', `デジタルマーガレット (Digital Margaret)`, 'https://digitalmargaret.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + +} \ No newline at end of file diff --git a/web/src/engine/websites/DigitalMargaret.webp b/web/src/engine/websites/DigitalMargaret.webp new file mode 100644 index 0000000000..dd80d39ab9 Binary files /dev/null and b/web/src/engine/websites/DigitalMargaret.webp differ diff --git a/web/src/engine/websites/DigitalMargaret_e2e.ts b/web/src/engine/websites/DigitalMargaret_e2e.ts new file mode 100644 index 0000000000..60f6ebe01c --- /dev/null +++ b/web/src/engine/websites/DigitalMargaret_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'digitalmargaret', + title: 'デジタルマーガレット (Digital Margaret)' + }, + container: { + url: 'https://digitalmargaret.jp/detail/dansou/', + id: '/detail/dansou/', + title: '策士な女装王子は男装令嬢とのいちゃラブをご所望です' + }, + child: { + id: '/contents/dansou/241101_1-1bd1f5e685898bb58781b42ad6094eb75/', + title: '第1-1話' + }, + entry: { + index: 0, + size: 1_570_077, + type: 'image/png', + timeout: 20000 + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/Futabanet.ts b/web/src/engine/websites/Futabanet.ts new file mode 100644 index 0000000000..c05820a695 --- /dev/null +++ b/web/src/engine/websites/Futabanet.ts @@ -0,0 +1,42 @@ +import { Tags } from '../Tags'; +import icon from './Futabanet.webp'; +import { Chapter, DecoratableMangaScraper, Page, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import type { Priority } from '../taskpool/DeferredTask'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +@Common.MangaCSS(/^https:\/\/gaugau\.futabane(t|x)\.jp\/list\/work\/[^/]+$/, 'ol.breadcrumb li:last-of-type') +@Common.MangasMultiPageCSS('/list/works?page={page}', 'div.works__grid div.list__box h4 a') + +export default class extends DecoratableMangaScraper { + + public constructor() { + super('futabanet', `がうがうモンスター (Futabanet Monster)`, 'https://gaugau.futabanet.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + + public override async FetchChapters(manga: Manga): Promise { + const request = new Request(`${this.URI.origin}${manga.Identifier}/episodes`); + const data = await FetchCSS(request, 'div.episode__grid a'); + return data.filter(chapter => !chapter.pathname.endsWith('/app')) + .map(chapter => { + const epnum = chapter.querySelector('.episode__num').textContent.trim(); + const title = chapter.querySelector('.episode__title').textContent.trim(); + return new Chapter(this, manga, chapter.pathname, title ? [epnum, title].join(' - ') : epnum); + }); + } + + public override async FetchPages(chapter: Chapter): Promise { //Not sure if needed anymore + let pages: Page[] = await Common.FetchPagesSinglePageCSS.call(this, chapter, 'div.works_tateyomi__img img'); + pages = pages?.map(page => new Page(this, chapter, page.Link, { useCommon: true })); + return pages?.length > 0 ? pages : await SpeedBinb.FetchPagesSinglePageAjax.call(this, chapter, SpeedBindVersion.v016130); + } + + public override async FetchImage(page: Page, priority: Priority, signal: AbortSignal): Promise { //Not sure if needed anymore + return page.Parameters?.useCommon ? Common.FetchImageAjax.call(this, page, priority, signal) : SpeedBinb.FetchImageAjax.call(this, page, priority, signal); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/Futabanet.webp b/web/src/engine/websites/Futabanet.webp similarity index 100% rename from web/src/engine/websites/legacy/Futabanet.webp rename to web/src/engine/websites/Futabanet.webp diff --git a/web/src/engine/websites/Futabanet_e2e.ts b/web/src/engine/websites/Futabanet_e2e.ts new file mode 100644 index 0000000000..b717d716c5 --- /dev/null +++ b/web/src/engine/websites/Futabanet_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'futabanet', + title: 'がうがうモンスター (Futabanet Monster)' + }, + container: { + url: 'https://gaugau.futabanet.jp/list/work/62591706776561c83f010000', + id: '/list/work/62591706776561c83f010000', + title: 'ポイントギフター《経験値分配能力者》の異世界最強ソロライフ~ブラックギルドから解放された男は万能最強職として無双する~' + }, + child: { + id: '/list/work/62591706776561c83f010000/episodes/1', + title: '第1話', + timeout: 10000 + }, + entry: { + index: 0, + size: 1_872_190, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/MangaPlanet.ts b/web/src/engine/websites/MangaPlanet.ts new file mode 100644 index 0000000000..4b7a6e3784 --- /dev/null +++ b/web/src/engine/websites/MangaPlanet.ts @@ -0,0 +1,58 @@ +import { Tags } from '../Tags'; +import icon from './MangaPlanet.webp'; +import { Chapter, DecoratableMangaScraper, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS, FetchWindowScript } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(element: HTMLElement) { + return { + id: element.querySelector('a').pathname, + title: element.querySelector('h3').innerText.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/comic\/[^/]+$/, '.card-body.book-detail h3') +@Common.MangasMultiPageCSS('/browse/title?ttlpage={page}', 'div#Title .row.book-list', 1, 1, 0, MangaExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130, true) +@SpeedBinb.ImageAjax() + +export default class extends DecoratableMangaScraper { + public constructor() { + super('mangaplanet', `MangaPlanet`, 'https://mangaplanet.com', Tags.Language.English, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + + public override async Initialize(): Promise { + return FetchWindowScript(new Request(this.URI), `window.cookieStore.set('mpaconf', '18')`); + } + + public override async FetchChapters(manga: Manga): Promise { + const data = await FetchCSS(new Request(new URL(manga.Identifier, this.URI)), '#accordion div[id*="vol_"]'); + const chapters : Chapter[] = []; + for (const volume of data) { + const title = volume.querySelector('h3').textContent.trim() + " - "; + + for (const chapter of [...volume.querySelectorAll(".list-group")].filter(e => e.querySelector('a') != null)) { + const origurl = /'([a-z0-9:/.?=]*)'/g.exec(chapter.querySelector('a').getAttribute("@click"))[1]; + //the chapter site gives different urls. sometimes you first get redirected to a login or 18+ restricted page other times not. + let chapid = origurl.split("/").slice(-1)[0]; + if (chapid.includes("?")) { + chapid = chapid.split("=").slice(-1)[0];//gets the id of the cid parameter + } + let url = ""; + if (origurl.includes("/reader")) { + url = "/reader?cid=" + chapid; + } else if (origurl.includes("/viewer")) { + url = "https://image.mangaplanet.com/viewer/" + chapid; + } + chapters.push(new Chapter(this, manga, url, title + chapter.querySelector('span').innerText.trim())); + } + } + return chapters; + } + +} \ No newline at end of file diff --git a/web/src/engine/websites/MangaPlanet.webp b/web/src/engine/websites/MangaPlanet.webp new file mode 100644 index 0000000000..a5a04787ab Binary files /dev/null and b/web/src/engine/websites/MangaPlanet.webp differ diff --git a/web/src/engine/websites/MangaPlanet_e2e.ts b/web/src/engine/websites/MangaPlanet_e2e.ts new file mode 100644 index 0000000000..d00d08331c --- /dev/null +++ b/web/src/engine/websites/MangaPlanet_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'mangaplanet', + title: 'MangaPlanet' + }, + container: { + url: 'https://mangaplanet.com/comic/64eeceb86e1e3', + id: '/comic/64eeceb86e1e3', + title: 'Concerned About My Virginity: I Wanna Give It to My Boss!', + }, + child: { + id: '/reader?cid=64f300af38575', + title: 'Volume 1 - Free Preview' + }, + entry: { + index: 0, + size: 1_709_506, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/MangaPlaza.ts b/web/src/engine/websites/MangaPlaza.ts new file mode 100644 index 0000000000..8ddd605074 --- /dev/null +++ b/web/src/engine/websites/MangaPlaza.ts @@ -0,0 +1,68 @@ +import { Tags } from '../Tags'; +import icon from './MangaPlaza.webp'; +import { Chapter, DecoratableMangaScraper, type Manga, type MangaPlugin } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS, FetchJSON, FetchWindowScript } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +type APIChapterResult = { + data: { + html_content: string, + html_page: string + } +} + +@Common.MangaCSS(/^{origin}\/title\/\d+\/$/, 'div.mainTitle h1.titleTxt') +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130, true) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('mangaplaza', `MangaPlaza`, ' https://mangaplaza.com', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override async Initialize(): Promise { + return FetchWindowScript(new Request(this.URI), `window.cookieStore.set('mp_over18_agreement', 'ON')`); + } + + public override async FetchMangas(provider: MangaPlugin): Promise { + //fetch genre list + let genres = (await FetchCSS(new Request(new URL('/genre/', this.URI)), 'a[href*="/genre/"]')).filter(link => link.pathname.match(/genre\/\d+\/$/)).map(link => link.pathname.match(/\d+/)[0]); + genres = Array.from(new Set(genres)); + const mangaList: Manga[] = []; + for (const genre of genres) { + mangaList.push(... await Common.FetchMangasMultiPageCSS.call(this, provider, `/genre/${genre}/?page={page}`, 'ul.listBox li div.titleName a')); + } + return mangaList.distinct(); + } + + public override async FetchChapters(manga: Manga): Promise { + const chapters: Chapter[] = []; + const mangaid = manga.Identifier.match(/title\/(\d+)/)[1]; + let page = 1; + let pageMax = 1; + + while (page <= pageMax) { + const request = new Request(new URL(`/api/title/content_list/?title_id=${mangaid}&content_id=0&page=${page}&order=down&_=${Date.now().toString()}`, this.URI)); + const { data } = await FetchJSON(request); + const chaptersNodes = [...new DOMParser().parseFromString(data.html_content, 'text/html').querySelectorAll('ul.detailBox div.inner_table')]; + for (const chapterNode of chaptersNodes) { + const title = chapterNode.querySelector('p.titleName').textContent.trim(); + const linkNode = chapterNode.querySelector('div.btnBlock a.prevBtn'); + if (linkNode) chapters.push(new Chapter(this, manga, linkNode.pathname + linkNode.search, title)); + } + + if (page == 1 && data.html_page != '') { + pageMax = parseInt(new DOMParser().parseFromString(data.html_page, 'text/html').querySelector('ul#_pages li:nth-last-of-type(2) a').dataset.page); + } + page++; + } + + return chapters.distinct(); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/MangaPlaza.webp b/web/src/engine/websites/MangaPlaza.webp new file mode 100644 index 0000000000..9b3efda508 Binary files /dev/null and b/web/src/engine/websites/MangaPlaza.webp differ diff --git a/web/src/engine/websites/MangaPlaza_e2e.ts b/web/src/engine/websites/MangaPlaza_e2e.ts new file mode 100644 index 0000000000..c052010361 --- /dev/null +++ b/web/src/engine/websites/MangaPlaza_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'mangaplaza', + title: 'MangaPlaza' + }, + container: { + url: 'https://mangaplaza.com/title/0303001706/', + id: '/title/0303001706/', + title: 'Defying Kurosaki-kun' + }, + child: { + id: '/reader/103030017060001/?return_url=https%3A%2F%2Fmangaplaza.com%2Ftitle%2F0303001706%2F%3Forder%3Ddown%26content_id%3D103030017060001', + title: '#1', + timeout: 10000 + }, + entry: { + index: 0, + size: 79_997, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/MichiKusa.ts b/web/src/engine/websites/MichiKusa.ts new file mode 100644 index 0000000000..7adcb59884 --- /dev/null +++ b/web/src/engine/websites/MichiKusa.ts @@ -0,0 +1,35 @@ +import { Tags } from '../Tags'; +import icon from './MichiKusa.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaInfoExtractor(element: HTMLElement) { + return { + id: element.querySelector('a').pathname, + title: element.querySelector('div.contents-info div.title').textContent.trim(), + }; +} +function ChapterExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname.replace(/index\.html$/, ''), + title: anchor.text.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/product\/[^/]+$/, 'header.entry-header h1.page-title') +@Common.MangasMultiPageCSS('/product/page/{page}', 'div.entry-content', 1, 1, 0, MangaInfoExtractor) +@Common.ChaptersSinglePageCSS('div.released_episodes div.items div.item a', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('michikusa', `MichiKusa`, 'https://michikusacomics.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } +} \ No newline at end of file diff --git a/web/src/engine/websites/MichiKusa.webp b/web/src/engine/websites/MichiKusa.webp new file mode 100644 index 0000000000..df2e13f1b7 Binary files /dev/null and b/web/src/engine/websites/MichiKusa.webp differ diff --git a/web/src/engine/websites/MichiKusa_e2e.ts b/web/src/engine/websites/MichiKusa_e2e.ts new file mode 100644 index 0000000000..c559e52410 --- /dev/null +++ b/web/src/engine/websites/MichiKusa_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'michikusa', + title: 'MichiKusa' + }, + container: { + url: 'https://michikusacomics.jp/product/myfirstblue', + id: '/product/myfirstblue', + title: 'きらきら、あおい', + }, + child: { + id: '/wp-content/uploads/data/20_myfirstblue/01/', + title: 'Ep.1', + }, + entry: { + index: 0, + size: 2_245_849, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/Ohtabooks.ts b/web/src/engine/websites/Ohtabooks.ts new file mode 100644 index 0000000000..3526ee982a --- /dev/null +++ b/web/src/engine/websites/Ohtabooks.ts @@ -0,0 +1,45 @@ +import { Tags } from '../Tags'; +import icon from './Ohtabooks.webp'; +import { Chapter, DecoratableMangaScraper, type Manga, type Page } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.querySelector('.title').textContent.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/[^/]+\/$/, 'h2.contentTitle') +@Common.MangasSinglePagesCSS(['/list/'], 'div.bnrList ul li a', MangaExtractor) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('ohtabooks', `Ohtabooks`, 'https://webcomic.ohtabooks.com', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override async FetchPages(chapter: Chapter): Promise { + //find real reader url to send to SpeedBinb, since redirection is done by Javascript + const [ data ] = await FetchCSS(new Request(chapter.Identifier), 'body'); + const reallink = data.innerHTML.match(/location.href='(.*)'/).at(1); + return SpeedBinb.FetchPagesSinglePageAjax.call(this, new Chapter(this, chapter.Parent as Manga, reallink, chapter.Title), SpeedBindVersion.v016130); + } + + public override async FetchChapters(manga: Manga): Promise { + const data = await FetchCSS(new Request(new URL(manga.Identifier, this.URI)), 'a[onClick^="return !openBook("]'); + const chapterList = data.map(element => { + const partId = element.getAttribute('onclick').match(/\d+/).at(0); + const title = element.querySelector('.title') ? element.querySelector('.title').textContent : element.querySelector('btnMini') ? element.querySelector('btnMini').textContent : 'マンガをよむ'; + return new Chapter(this, manga, `https://yondemill.jp/contents/${partId}?view=1&u0=1`, title.trim()); + }); + return chapterList.reverse().distinct(); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/Ohtabooks.webp b/web/src/engine/websites/Ohtabooks.webp similarity index 100% rename from web/src/engine/websites/legacy/Ohtabooks.webp rename to web/src/engine/websites/Ohtabooks.webp diff --git a/web/src/engine/websites/Ohtabooks_e2e.ts b/web/src/engine/websites/Ohtabooks_e2e.ts new file mode 100644 index 0000000000..c6a5a03ff4 --- /dev/null +++ b/web/src/engine/websites/Ohtabooks_e2e.ts @@ -0,0 +1,26 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'ohtabooks', + title: 'Ohtabooks' + }, + container: { + url: 'https://webcomic.ohtabooks.com/kumazo/', + id: '/kumazo/', + title: 'クマ蔵とゲームなご主人', + }, + child: { + id: 'https://yondemill.jp/contents/55155?view=1&u0=1', + title: '第1話「ご主人とゲーム」', + timeout: 10000 + }, /*since page id redirect to another url, test is failing despites us getting images + https://yondemill.jp/contents/55155?view=1&u0=1' redirect to https://binb.bricks.pub/contents/b0baf48e-9a1e-4ac7-b331-f4b978714dc7_1673856049/speed_reader + entry: { + index: 0, + size: 1_385_998, + type: 'image/png' + }*/ +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/OneTwoThreeHon.ts b/web/src/engine/websites/OneTwoThreeHon.ts new file mode 100644 index 0000000000..ccc699da27 --- /dev/null +++ b/web/src/engine/websites/OneTwoThreeHon.ts @@ -0,0 +1,39 @@ +import { Tags } from '../Tags'; +import icon from './OneTwoThreeHon.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaInfoExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.pathname.match(/[^/]+\/web-comic\/([^/]+)\//)[1] + }; +} +function ChapterExtractor(element: HTMLLIElement) { + return { + id: new URL(element.querySelector('a').pathname.replace(/index.html$/, ''), this.URI).pathname, + title: element.innerText.match(/\s*(.*?)\s+/).at(1) + }; +} + +@Common.MangaCSS(/^{origin}\/[^/]+\/web-comic\/[^/]+\/$/, 'div.title-area h2') +@Common.MangasSinglePagesCSS(['/polca/web-comic/', '/nova/web-comic/'], 'ul.comic__list > li > a', MangaInfoExtractor) +@Common.ChaptersSinglePageCSS('div.read-episode li:has(a)', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('onetwothreehon', `123hon`, 'https://www.123hon.com', Tags.Media.Manga, Tags.Language.Japanese, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override async Initialize(): Promise { + //do nothing, as https://www.123hon.com fails to load but https://www.123hon.com/nova and https://www.123hon.com/polca works + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/OneTwoThreeHon.webp b/web/src/engine/websites/OneTwoThreeHon.webp similarity index 100% rename from web/src/engine/websites/legacy/OneTwoThreeHon.webp rename to web/src/engine/websites/OneTwoThreeHon.webp diff --git a/web/src/engine/websites/OneTwoThreeHon_e2e.ts b/web/src/engine/websites/OneTwoThreeHon_e2e.ts new file mode 100644 index 0000000000..dc5f4d0c83 --- /dev/null +++ b/web/src/engine/websites/OneTwoThreeHon_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'onetwothreehon', + title: '123hon' + }, + container: { + url: 'https://www.123hon.com/polca/web-comic/spxsp/', + id: '/polca/web-comic/spxsp/', + title: '異世界温泉冒険譚~スプラッシュ×スプラッシュ~', + }, + child: { + id: '/vw/spxsp/sv_pt000644b1883c326d_1a/', + title: '第1話(1/2)' + }, + entry: { + index: 0, + size: 1_468_880, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/SManga.ts b/web/src/engine/websites/SManga.ts new file mode 100644 index 0000000000..6824e1200f --- /dev/null +++ b/web/src/engine/websites/SManga.ts @@ -0,0 +1,54 @@ +import { Tags } from '../Tags'; +import icon from './SManga.webp'; +import { Chapter, DecoratableMangaScraper, type MangaPlugin, Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchWindowScript } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +type SSD = { + datas?: [{ + series_data: { + series_name: string, + series_id: number + } + }], + data?: { + item_datas?: [{ + ssid: number, + isbn: string, + item_name: string + }] + } +} + +@Common.MangasNotSupported() +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('smanga', `S-Manga`, 'https://www.s-manga.net', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + public override ValidateMangaURL(url: string): boolean { + return /https:\/\/www\.s-manga\.net\/items\/contents.html\?isbn=/.test(url); + } + + public override async FetchManga(provider: MangaPlugin, url: string): Promise { + const { datas } = await FetchWindowScript(new Request(url), 'window.ssd', 2000); + return new Manga(this, provider, datas[0].series_data.series_id.toString(), datas[0].series_data.series_name.trim()); + } + + public override async FetchChapters(manga: Manga): Promise { + const url = new URL(`/search/search.html?seriesid=${manga.Identifier}&order=1`, this.URI); + const { data: { item_datas } } = await FetchWindowScript(new Request(url), 'window.ssd', 2000); + return item_datas.map(chapter => new Chapter(this, manga, `/reader/main.php?cid=${this.IsbnToCid(chapter.isbn)}`, chapter.item_name.replace(manga.Title, '').trim().replace(/^//, '').trim())); + } + + private IsbnToCid(isbn: string): string { + return isbn.replaceAll('-', ''); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/SManga.webp b/web/src/engine/websites/SManga.webp new file mode 100644 index 0000000000..dcb9bfff41 Binary files /dev/null and b/web/src/engine/websites/SManga.webp differ diff --git a/web/src/engine/websites/SManga_e2e.ts b/web/src/engine/websites/SManga_e2e.ts new file mode 100644 index 0000000000..1ad5761a24 --- /dev/null +++ b/web/src/engine/websites/SManga_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'smanga', + title: 'S-Manga' + }, + container: { + url: 'https://www.s-manga.net/items/contents.html?isbn=978-4-08-883785-7', + id: '35169', + title: 'ONE PIECE', + timeout: 15000 + }, + child: { + id: '/reader/main.php?cid=9784088837857', + title: '107' + }, + entry: { + index: 0, + size: 2_613_081, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/TKSuperheroComics.ts b/web/src/engine/websites/TKSuperheroComics.ts new file mode 100644 index 0000000000..9ba8470052 --- /dev/null +++ b/web/src/engine/websites/TKSuperheroComics.ts @@ -0,0 +1,34 @@ +import { Tags } from '../Tags'; +import icon from './TKSuperheroComics.webp'; +import { Chapter, DecoratableMangaScraper, type Manga } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchCSS} from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; +function MangaExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.querySelector('.rensai-episode-title').textContent.trim() + }; +} + +@Common.MangaCSS(/{origin}\/rensai\/[^/]+\/$/, 'div.manga-overview-top-wrapper h2.manga-heading') +@Common.MangasSinglePagesCSS(['/rensai'], 'li.rensai-episode-list a', MangaExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016061) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('tksuperherocomics', `Televi-kun Superhero Comics (てれびくんスーパーヒーローコミックス)`, 'https://televikun-super-hero-comics.com', Tags.Media.Manga, Tags.Language.Japanese, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override async FetchChapters(manga: Manga): Promise { + const chapters = await FetchCSS(new Request(new URL(manga.Identifier, this.URI)), 'section[data-start-date].manga-all-episode-section li.manga-all-episode-list a:not([href^="http"])'); + return chapters.map(chapter => new Chapter(this, manga, `${manga.Identifier}${chapter.getAttribute('href')}`, chapter.text.trim())); + } + +} \ No newline at end of file diff --git a/web/src/engine/websites/TKSuperheroComics.webp b/web/src/engine/websites/TKSuperheroComics.webp new file mode 100644 index 0000000000..b4e7b647bc Binary files /dev/null and b/web/src/engine/websites/TKSuperheroComics.webp differ diff --git a/web/src/engine/websites/TKSuperheroComics_e2e.ts b/web/src/engine/websites/TKSuperheroComics_e2e.ts new file mode 100644 index 0000000000..dee1e92c55 --- /dev/null +++ b/web/src/engine/websites/TKSuperheroComics_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'tksuperherocomics', + title: 'Televi-kun Superhero Comics (てれびくんスーパーヒーローコミックス)' + }, + container: { + url: 'https://televikun-super-hero-comics.com/rensai/theheroisinside/', + id: '/rensai/theheroisinside/', + title: 'ヒーローは中にいる!' + }, + child: { + id: '/rensai/theheroisinside/episode-017/', + title: '17話 合宿スタート!!', + }, + entry: { + index: 0, + size: 1_867_269, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/Yanmaga.ts b/web/src/engine/websites/Yanmaga.ts new file mode 100644 index 0000000000..63a4495530 --- /dev/null +++ b/web/src/engine/websites/Yanmaga.ts @@ -0,0 +1,48 @@ +import { Tags } from '../Tags'; +import icon from './Yanmaga.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(anchor: HTMLAnchorElement) { + return { + id: anchor.pathname, + title: anchor.querySelector('.mod-book-title').textContent.trim() + }; +} + +const chapterScript = ` + new Promise(resolve => { + const interval = setInterval(() => { + let morebtn = document.querySelector('.mod-episode-more-button') ; + if (morebtn) morebtn.click() + else { + clearInterval(interval); + const chapters = [...document.querySelectorAll('a.mod-episode-link')]; + resolve(chapters.map(chapter => { + return { + id: chapter.pathname, + title: chapter.querySelector('.mod-episode-title').textContent.trim() + } + })); + } + }, 1000); + }); +`; + +@Common.MangaCSS(/^{origin}\/comics\/[^/]+$/, 'h1.detail-header-title, h1.detailv2-outline-title') +@Common.MangasSinglePagesCSS(['/comics'], 'a.ga-comics-book-item', MangaExtractor) +@Common.ChaptersSinglePageJS(chapterScript, 200) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130, true) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('yanmaga', `Yanmaga`, 'https://yanmaga.jp', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + public override get Icon() { + return icon; + } + +} \ No newline at end of file diff --git a/web/src/engine/websites/Yanmaga.webp b/web/src/engine/websites/Yanmaga.webp new file mode 100644 index 0000000000..a4115f158d Binary files /dev/null and b/web/src/engine/websites/Yanmaga.webp differ diff --git a/web/src/engine/websites/Yanmaga_e2e.ts b/web/src/engine/websites/Yanmaga_e2e.ts new file mode 100644 index 0000000000..e732d1016b --- /dev/null +++ b/web/src/engine/websites/Yanmaga_e2e.ts @@ -0,0 +1,24 @@ +import { TestFixture, type Config } from '../../../test/WebsitesFixture'; + +const config: Config = { + plugin: { + id: 'yanmaga', + title: 'Yanmaga' + }, + container: { + url: 'https://yanmaga.jp/comics/%E5%A4%A2%E3%81%86%E3%81%A4%E3%81%A4%E3%81%AE%E8%8A%B1%E3%81%AE%E5%9C%92', + id: '/comics/%E5%A4%A2%E3%81%86%E3%81%A4%E3%81%A4%E3%81%AE%E8%8A%B1%E3%81%AE%E5%9C%92', + title: '夢うつつの花の園', + }, + child: { + id: '/comics/%E5%A4%A2%E3%81%86%E3%81%A4%E3%81%A4%E3%81%AE%E8%8A%B1%E3%81%AE%E5%9C%92/351192c0f7d1cf3b88175f3d9dfae594', + title: '第1夢 幽体離脱' + }, + entry: { + index: 0, + size: 2_088_305, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/YoMonga.ts b/web/src/engine/websites/YoMonga.ts new file mode 100644 index 0000000000..0e80d7a85f --- /dev/null +++ b/web/src/engine/websites/YoMonga.ts @@ -0,0 +1,36 @@ +import { Tags } from '../Tags'; +import icon from './YoMonga.webp'; +import { DecoratableMangaScraper } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +function MangaExtractor(element: HTMLElement) { + return { + id: element.querySelector('a').pathname, + title: element.querySelector('div.book-box4-title').textContent.trim() + }; +} + +function ChapterExtractor(element: HTMLElement) { + const anchor = element.querySelector('a'); + return { + id: anchor.pathname + anchor.search, + title: element.querySelector('span').textContent.trim() + }; +} + +@Common.MangaCSS(/^{origin}\/titles\/\d+\//, 'div.intr-title') +@Common.MangasMultiPageCSS('/titles/?page_num={page}', 'div.book-box4', 1, 1, 0, MangaExtractor) +@Common.ChaptersSinglePageCSS('div.episode-list', ChapterExtractor) +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016130) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + public constructor() { + super('yomonga', `YoMonga`, 'https://www.yomonga.com', Tags.Language.Japanese, Tags.Media.Manga, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } +} \ No newline at end of file diff --git a/web/src/engine/websites/YoMonga.webp b/web/src/engine/websites/YoMonga.webp new file mode 100644 index 0000000000..58a636f82a Binary files /dev/null and b/web/src/engine/websites/YoMonga.webp differ diff --git a/web/src/engine/websites/YoMonga_e2e.ts b/web/src/engine/websites/YoMonga_e2e.ts new file mode 100644 index 0000000000..d61938dad7 --- /dev/null +++ b/web/src/engine/websites/YoMonga_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'yomonga', + title: 'YoMonga' + }, + container: { + url: 'https://www.yomonga.com/titles/306/?episode=113', + id: '/titles/306/', + title: '部長と社畜の恋はもどかしい' + }, + child: { + id: '/titles/306/?episode=1', + title: 'Chapter.1_1巻', + timeout: 20000 + }, + entry: { + index: 0, + size: 4_190_733, + type: 'image/png' + } +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/YoungJump.ts b/web/src/engine/websites/YoungJump.ts new file mode 100644 index 0000000000..28fe3b675a --- /dev/null +++ b/web/src/engine/websites/YoungJump.ts @@ -0,0 +1,43 @@ +import { Tags } from '../Tags'; +import icon from './YoungJump.webp'; +import { DecoratableMangaScraper, Manga, type MangaPlugin } from '../providers/MangaPlugin'; +import * as Common from './decorators/Common'; +import * as SpeedBinb from './decorators/SpeedBinb'; +import { FetchJSON, FetchWindowScript } from '../platform/FetchProvider'; +import { SpeedBindVersion } from './decorators/SpeedBinb'; + +type APIMagazine = { + url: string, + issue: string, + number: string +} + +@Common.ChaptersUniqueFromManga() +@SpeedBinb.PagesSinglePageAjax(SpeedBindVersion.v016201, true) +@SpeedBinb.ImageAjax() +export default class extends DecoratableMangaScraper { + + public constructor() { + super('youngjump', `ヤングジャンプ / ウルトラジャンプ (young jump/ultra jump)`, 'https://www.youngjump.world', Tags.Media.Manga, Tags.Language.Japanese, Tags.Source.Official); + } + + public override get Icon() { + return icon; + } + + public override ValidateMangaURL(url: string): boolean { + return /^https:\/\/www\.youngjump\.world\/reader\/reader.html\?/.test(url); + } + + public override async FetchManga(provider: MangaPlugin, url: string): Promise { + const mangatitle = await FetchWindowScript(new Request(url), 'document.title', 3000); + const uri = new URL(url); + return new Manga(this, provider, uri.pathname + uri.search, mangatitle.trim()); + + } + + public override async FetchMangas(provider: MangaPlugin): Promise { + const data = await FetchJSON(new Request(new URL('/yj-rest-apis/getBookInfo.php', this.URI))); + return data.map(magazine => new Manga(this, provider, magazine.url, `${magazine.issue} - ${magazine.number}`.trim())); + } +} \ No newline at end of file diff --git a/web/src/engine/websites/legacy/YoungJump.webp b/web/src/engine/websites/YoungJump.webp similarity index 100% rename from web/src/engine/websites/legacy/YoungJump.webp rename to web/src/engine/websites/YoungJump.webp diff --git a/web/src/engine/websites/YoungJump_e2e.ts b/web/src/engine/websites/YoungJump_e2e.ts new file mode 100644 index 0000000000..9c6b2945c0 --- /dev/null +++ b/web/src/engine/websites/YoungJump_e2e.ts @@ -0,0 +1,25 @@ +import { TestFixture } from '../../../test/WebsitesFixture'; + +const config = { + plugin: { + id: 'youngjump', + title: 'となりのヤングジャンプ (Tonari no Young Jump)' + }, + /* Content is accessible after login + container: { + url: 'https://www.youngjump.world/reader/reader.html?cid=101012340&u1=10001', + id: '/reader/reader.html?cid=101012340&u1=10001', + title: '俺だけ不遇スキルの異世界召喚叛逆記~最弱スキル【吸収】が全てを飲み込むまで~' + }, + child: { + id: '/reader/reader.html?cid=101012340&u1=10001', + title: 'YJ2024_01 - 1' + }, + entry: { + index: 0, + size: 4_627_863, + type: 'image/png' + }*/ +}; + +new TestFixture(config).AssertWebsite(); \ No newline at end of file diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts index 1437c61e1f..5e13382047 100755 --- a/web/src/engine/websites/_index.ts +++ b/web/src/engine/websites/_index.ts @@ -51,6 +51,8 @@ export { default as Bokugents } from './Bokugents'; export { default as Bomtoon } from './Bomtoon'; export { default as BomtoonCN } from './BomtoonCN'; export { default as Bontoon } from './Bontoon'; +export { default as BookHodai } from './BookHodai'; +export { default as BookLive } from './BookLive'; export { default as Boomtoon } from './Boomtoon'; export { default as CartelDeManhwas } from './CartelDeManhwas'; export { default as CarToonMad } from './CarToonMad'; @@ -59,12 +61,14 @@ export { default as CatTranslator } from './CatTranslator'; export { default as Chochox } from './Chochox'; export { default as CiaoPlus } from './CiaoPlus'; export { default as CMangax } from './CMangax'; +export { default as Cmoa } from './Cmoa'; export { default as Cocorip } from './Cocorip'; export { default as CoffeeManga } from './CoffeeManga'; export { default as ColaManga } from './ColaManga'; export { default as ColoredManga } from './ColoredManga'; export { default as ComicAction } from './ComicAction'; export { default as ComicBorder } from './ComicBorder'; +export { default as ComicBrise } from './ComicBrise'; export { default as ComicDays } from './ComicDays'; export { default as ComicEarthStar } from './ComicEarthStar'; export { default as ComicExtra } from './ComicExtra'; @@ -73,11 +77,15 @@ export { default as ComicFuz } from './ComicFuz'; export { default as ComicGardo } from './ComicGardo'; export { default as ComicGrowl } from './ComicGrowl'; export { default as ComicK } from './ComicK'; +export { default as ComicMeteor } from './ComicMeteor'; export { default as Comico } from './Comico'; +export { default as ComicPolaris } from './ComicPolaris'; +export { default as ComicPorta } from './ComicPorta'; export { default as ComicRyu } from './ComicRyu'; export { default as ComicsValley } from './ComicsValley'; export { default as ComicTop } from './ComicTop'; export { default as ComicTrail } from './ComicTrail'; +export { default as ComicValkyrie } from './ComicValkyrie'; export { default as ComicVn } from './ComicVn'; export { default as ComicZenon } from './ComicZenon'; export { default as ComicZerosum } from './ComicZerosum'; @@ -113,6 +121,7 @@ export { default as DEXHentai } from './DEXHentai'; export { default as DiamondFansub } from './DiamondFansub'; export { default as DianxiaTrads } from './DianxiaTrads'; export { default as Digimon } from './Digimon'; +export { default as DigitalMargaret } from './DigitalMargaret'; export { default as DigitalTeam } from './DigitalTeam'; export { default as DingManhua } from './DingManhua'; export { default as DisasterScans } from './DisasterScans'; @@ -150,6 +159,7 @@ export { default as FMTeam } from './FMTeam'; export { default as FoyScan } from './FoyScan'; export { default as FreeComicOnline } from './FreeComicOnline'; export { default as FreeManga } from './FreeManga'; +export { default as Futabanet } from './Futabanet'; export { default as Futari } from './Futari'; export { default as GaiaToon } from './GaiaToon'; export { default as GalaxyManga } from './GalaxyManga'; @@ -366,6 +376,8 @@ export { default as MangaOwlio } from './MangaOwlio'; export { default as MangaPanda } from './MangaPanda'; export { default as MangaParkPublisher } from './MangaParkPublisher'; export { default as MangaPill } from './MangaPill'; +export { default as MangaPlanet } from './MangaPlanet'; +export { default as MangaPlaza } from './MangaPlaza'; export { default as MangaPro } from './MangaPro'; export { default as MangaRaw1001 } from './MangaRaw1001'; export { default as MangaRawAC } from './MangaRawAC'; @@ -458,6 +470,7 @@ export { default as Mgkomik } from './Mgkomik'; export { default as MHScans } from './MHScans'; export { default as Mi2mangaES } from './Mi2mangaES'; export { default as MiauScan } from './MiauScan'; +export { default as MichiKusa } from './MichiKusa'; export { default as Migudm } from './Migudm'; export { default as Mikoroku } from './Mikoroku'; export { default as MilaSub } from './MilaSub'; @@ -504,8 +517,10 @@ export { default as Noromax } from './Noromax'; export { default as NovelMic } from './NovelMic'; export { default as NoxScans } from './NoxScans'; export { default as NyxScans } from './NyxScans'; +export { default as Ohtabooks } from './Ohtabooks'; export { default as OlimpoScans } from './OlimpoScans'; export { default as OlympusScanlation } from './OlympusScanlation'; +export { default as OneTwoThreeHon } from './OneTwoThreeHon'; export { default as OnMangaMe } from './OnMangaMe'; export { default as Opiatoon } from './Opiatoon'; export { default as Oremanga } from './Oremanga'; @@ -623,6 +638,7 @@ export { default as SixParkbbsClub } from './SixParkbbsClub'; export { default as SixParkbbsWeb } from './SixParkbbsWeb'; export { default as SkyManga } from './SkyManga'; export { default as SkyMangas } from './SkyMangas'; +export { default as SManga } from './SManga'; export { default as SoftKomik } from './SoftKomik'; export { default as SoulScans } from './SoulScans'; export { default as SpiderScans } from './SpiderScans'; @@ -656,6 +672,7 @@ export { default as TheBlank } from './TheBlank'; export { default as ThreeHentai } from './ThreeHentai'; export { default as ThunderScans } from './ThunderScans'; export { default as TitanManga } from './TitanManga'; +export { default as TKSuperheroComics } from './TKSuperheroComics'; export { default as TmoManga } from './TmoManga'; export { default as ToCoronaEx } from './ToCoronaEx'; export { default as TonariNoYoungJump } from './TonariNoYoungJump'; @@ -729,6 +746,7 @@ export { default as XManhwa } from './XManhwa'; export { default as XoxoComics } from './XoxoComics'; export { default as XXXYaoi } from './XXXYaoi'; export { default as YakshaScans } from './YakshaScans'; +export { default as Yanmaga } from './Yanmaga'; export { default as YanpFansub } from './YanpFansub'; export { default as YaoiChan } from './YaoiChan'; export { default as YaoiHavenReborn } from './YaoiHavenReborn'; @@ -737,7 +755,9 @@ export { default as YaoiScan } from './YaoiScan'; export { default as YaoiTR } from './YaoiTR'; export { default as YawarakaSpirits } from './YawarakaSpirits'; export { default as Ynjn } from './Ynjn'; +export { default as YoMonga } from './YoMonga'; export { default as YomuComics } from './YomuComics'; +export { default as YoungJump } from './YoungJump'; export { default as YumeKomik } from './YumeKomik'; export { default as YuraManga } from './YuraManga'; export { default as Yurineko } from './Yurineko'; @@ -752,19 +772,13 @@ export { default as ZinMangaNet } from './ZinMangaNet'; // Legacy Websites export { default as AnimePahe } from './legacy/AnimePahe'; export { default as ArazNovel } from './legacy/ArazNovel'; -export { default as BookLive } from './legacy/BookLive'; export { default as ComicBoost } from './legacy/ComicBoost'; -export { default as ComicBrise } from './legacy/ComicBrise'; export { default as ComicFire } from './legacy/ComicFire'; export { default as COMICMeDu } from './legacy/COMICMeDu'; -export { default as ComicMeteor } from './legacy/ComicMeteor'; -export { default as ComicPolaris } from './legacy/ComicPolaris'; -export { default as ComicValkyrie } from './legacy/ComicValkyrie'; export { default as CrunchyAnime } from './legacy/CrunchyAnime'; export { default as CrunchyManga } from './legacy/CrunchyManga'; export { default as EHentai } from './legacy/EHentai'; export { default as EpikManga } from './legacy/EpikManga'; -export { default as Futabanet } from './legacy/Futabanet'; export { default as GammaPlus } from './legacy/GammaPlus'; export { default as Guoman8 } from './legacy/Guoman8'; export { default as KanMan } from './legacy/KanMan'; @@ -812,9 +826,7 @@ export { default as NovelcoolIT } from './legacy/NovelcoolIT'; export { default as NovelcoolRU } from './legacy/NovelcoolRU'; export { default as Novelgo } from './legacy/Novelgo'; export { default as NovelRingan } from './legacy/NovelRingan'; -export { default as Ohtabooks } from './legacy/Ohtabooks'; export { default as OnePieceTube } from './legacy/OnePieceTube'; -export { default as OneTwoThreeHon } from './legacy/OneTwoThreeHon'; export { default as PlotTwistNoFansub } from './legacy/PlotTwistNoFansub'; export { default as ShinobiScans } from './legacy/ShinobiScans'; export { default as SixMH7 } from './legacy/SixMH7'; @@ -844,6 +856,5 @@ export { default as WebComicGamma } from './legacy/WebComicGamma'; export { default as WoopRead } from './legacy/WoopRead'; export { default as WordRain } from './legacy/WordRain'; export { default as WuxiaWorld } from './legacy/WuxiaWorld'; -export { default as YoungJump } from './legacy/YoungJump'; export { default as ZinNovel } from './legacy/ZinNovel'; export { default as ZYMKMangaWeb } from './legacy/ZYMKMangaWeb'; \ No newline at end of file diff --git a/web/src/engine/websites/decorators/SpeedBinb.ts b/web/src/engine/websites/decorators/SpeedBinb.ts new file mode 100644 index 0000000000..ce020c1e87 --- /dev/null +++ b/web/src/engine/websites/decorators/SpeedBinb.ts @@ -0,0 +1,689 @@ +import { Fetch, FetchJSON, FetchWindowScript } from '../../platform/FetchProvider'; +import { type MangaScraper, type Chapter, Page } from '../../providers/MangaPlugin'; +import type { Priority } from '../../taskpool/TaskPool'; +import * as Common from './Common'; +import DeScramble from '../../transformers/ImageDescrambler'; + +type ViewerData = { + viewerUrl: URL; + SBHtmlElement: HTMLElement +} + +type RequestData = { + cid: string, + sharingKey: string, + dmytime, + u0: string, + u1: string, + request: Request +} + +type JSONPageData = { + items: ContentConfiguration[] +} + +type ContentConfiguration = { + ContentID: string, + ctbl: string | string[], + ptbl: string | string[], + ServerType: number | string + ContentsServer: string, + p: string, + ViewMode: number, + ContentDate: string +} + +type JSONImageData = { + resources: { + i: { + src: string + } + } + views: { + coords: string[], + width: number, + height: number + }[] +} + +type PageViewv016130 = { + transfers: { + index: number, + coords: DrawImageCoords[] + }[], + width: number, + height: number +} + +type DrawImageCoords = { + height: number, + width: number, + xdest: number, + xsrc: number, + ydest: number, + ysrc: number +} + +type SBCDATA = { + ttx: string; +} + +type DescrambleKP = { + s: string, + u: string +} + +type Dimensions = { + width: number, + height: number +} + +const JsonFetchScript = ` + new Promise((resolve, reject) => { + try { + fetch('{URI}') + .then(response => response.json()) + .then(json => resolve(json)) + } catch (error) { + reject(error); + } + }); +`; + +export enum SpeedBindVersion { v016061, v016452, v016201, v016130 }; + +function getSanitizedURL(base: string, append: string): URL { + const baseURI = new URL(append, base + '/'); + baseURI.pathname = baseURI.pathname.replaceAll(/\/\/+/g, '/'); + return baseURI; +} + +/** + * Return real chapter url & SpeedBinb "pages" element from said page, so we can work + * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method + * @param chapter - A reference to the {@link Chapter} which shall be assigned as parent for the extracted pages + */ +async function GetViewerData(this: MangaScraper, chapter: Chapter): Promise { + let viewerUrl = new URL(chapter.Identifier, this.URI); + const request = new Request(viewerUrl, { + headers: { + Referer: this.URI.origin + } + }); + + const response = await Fetch(request); + const dom = new DOMParser().parseFromString(await response.text(), 'text/html'); + const SBHtmlElement = dom.querySelector('div#content.pages'); + //handle redirection. Sometimes chapter is redirected + if (response.redirected) { + viewerUrl = new URL(response.url); + } + return { viewerUrl, SBHtmlElement }; +} +/** + * Create first SpeedBinb AJAX request to perform, using viewerUrl GET parameters, and endpoint from SpeedBinb HTML node + * @param viewerUrl - Read Url of the SpeedBinb Viewer + * @param sbHtmlElement - HTMLElement extracted from said page + */ +async function CreatePtBinbRequestData(viewerUrl: URL, sbHtmlElement: HTMLElement): Promise { + let cid = viewerUrl.searchParams.get('cid') ?? sbHtmlElement.dataset['ptbinbCid']; + + //in case cid is not in url and not in html, try to get it from page redirected by Javascript/ Meta element + if (!cid) { + cid = await FetchWindowScript(new Request(viewerUrl), 'new URL(window.location).searchParams.get("cid");', 5000); + } + if (!cid) throw new Error('Unable to find CID (content ID) !'); + + const sharingKey = _tt(cid); + const uri = getSanitizedURL(viewerUrl.href, sbHtmlElement.dataset.ptbinb); + const dmytime = Date.now().toString(); + uri.searchParams.set('cid', cid); + uri.searchParams.set('dmytime', dmytime); + uri.searchParams.set('k', sharingKey); + + const u0 = viewerUrl.searchParams.get('u0'); + const u1 = viewerUrl.searchParams.get('u1'); + if (u0) uri.searchParams.set('u0', u0); + if (u1) uri.searchParams.set('u1', u1); + + const request = new Request(uri, { + headers: { + Referer: viewerUrl.href + } + }); + + return { cid, sharingKey, dmytime, u0, u1, request }; +} + +/********************************************** + ******** Page List Extraction Methods ******** + **********************************************/ + +/** + * An extension method for extracting all pages for the given {@link chapter} using the given CSS {@link query}. + * The pages are extracted from the composed url based on the `Identifier` of the {@link chapter} and the `URI` of the website. + * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method + * @param chapter - A reference to the {@link Chapter} which shall be assigned as parent for the extracted pages + * @param version - SpeedBinb version used by the website + * @param needCookies - Use browser window to perform first JSON request to get access cookies properly + */ +export async function FetchPagesSinglePageAjax(this: MangaScraper, chapter: Chapter, version: SpeedBindVersion, needCookies = false): Promise { + const { viewerUrl, SBHtmlElement } = await GetViewerData.call(this, chapter); + if (version == SpeedBindVersion.v016061) { + //ComicBrise, ComicMeteor, ComicPorta, ComicValKyrie, DigitalMargaret, MichiKusa, OneTwoThreeHon, TKSuperheroComics + const [...imageConfigurations] = SBHtmlElement.querySelectorAll('div[data-ptimg$="ptimg.json"]'); + return imageConfigurations.map(element => new Page(this, chapter, getSanitizedURL(viewerUrl.href, element.dataset.ptimg))); + } + + const params = await CreatePtBinbRequestData(viewerUrl, SBHtmlElement); + const { items } = !needCookies ? await FetchJSON(params.request) : await FetchWindowScript(new Request(viewerUrl), JsonFetchScript.replace('{URI}', params.request.url), 2000); + return getPagesLinks.call(this, items[0], params, chapter, version); +} +/** + * A class decorator that adds the ability to extract all pages for a given chapter from a website using SpeedBinb Viewer. + * @param version - SpeedBinb version used by the website + * @param needCookies - Use browser window to perform first JSON request to get access cookies properly + */ + +export function PagesSinglePageAjax(version: SpeedBindVersion, needCookies = false) { + return function DecorateClass(ctor: T, context?: ClassDecoratorContext): T { + Common.ThrowOnUnsupportedDecoratorContext(context); + + return class extends ctor { + public async FetchPages(this: MangaScraper, chapter: Chapter): Promise { + return FetchPagesSinglePageAjax.call(this, chapter, version, needCookies); + } + }; + }; +} +async function getPagesLinks(this: MangaScraper, configuration: ContentConfiguration, params: RequestData, chapter: Chapter, version: SpeedBindVersion): Promise { + const cid = version === SpeedBindVersion.v016452 ? params.cid : configuration.ContentID; + configuration.ctbl = _pt(cid, params.sharingKey, configuration.ctbl as string); + configuration.ptbl = _pt(cid, params.sharingKey, configuration.ptbl as string); + try { + configuration.ServerType = parseInt(configuration.ServerType as string); + } catch { } + + switch (configuration.ServerType as number) { + case 0: { //v016130 Booklive , v016452 CMOA + const uri = getSanitizedURL(configuration.ContentsServer, 'sbcGetCntnt.php'); + uri.searchParams.set('cid', cid); + uri.searchParams.set('dmytime', configuration.ContentDate); + uri.searchParams.set('p', configuration.p); + uri.searchParams.set('vm', configuration.ViewMode.toString()); + if (version === SpeedBindVersion.v016452) { //CMOA + uri.searchParams.set('q', '1'); + uri.searchParams.set('u0', params.u0); + uri.searchParams.set('u1', params.u1); + } + return await ExtractPages.call(this, uri, '/sbcGetCntnt.php', '/sbcGetImg.php', configuration, chapter, true); + } + + case 1: {//v016130 Futabanet, BookHodai, Booklive, OhtaBooks, SManga + const uri = getSanitizedURL(configuration.ContentsServer, 'content.js'); + if (configuration.ContentDate) uri.searchParams.set('dmytime', configuration.ContentDate); + return await ExtractPages.call(this, uri, '/content.js', '{src}/M_H.jpg', configuration, chapter); + } + case 2: {//v016130 MangaPlanet, MangaPlaza, Yanmaga, Yomonga + const uri = getSanitizedURL(configuration.ContentsServer, 'content'); + if (configuration.ContentDate) uri.searchParams.set('dmytime', configuration.ContentDate); + if (version === SpeedBindVersion.v016201) uri.searchParams.set('u1', params.u1); //YOUNGJUMP + return await ExtractPages.call(this, uri, '/content', '/img/{src}', configuration, chapter); + } + } + return Promise.reject(new Error('Content server type not supported!')); +} + +async function ExtractPages(uri: URL, replaceFrom : string, replaceto: string, configuration: ContentConfiguration, chapter: Chapter, setSrc = false): Promise { + const response = await Fetch(new Request(uri, { headers: { Referer: this.URI.href } })); + const data = await response.text(); + const { ttx }: SBCDATA = data.startsWith('DataGet_Content(') ? JSON.parse(data.slice(16, -1)) : JSON.parse(data); + const dom = new DOMParser().parseFromString(ttx, 'text/html'); + const pageLinks = [...dom.querySelectorAll('t-case:first-of-type t-img')].map(img => { + let src = img.getAttribute('src'); + + const pageUri = new URL(uri); + pageUri.hash = window.btoa(JSON.stringify(lt_001(src, configuration.ctbl as string[], configuration.ptbl as string[]))); + if (setSrc) pageUri.searchParams.set('src', src); + + if (!src.startsWith('/')) src = '/' + src; + pageUri.href = pageUri.href.replace(replaceFrom, replaceto.replace('{src}', src)); + return new Page(this, chapter, pageUri); + + }); + return pageLinks; +} + +/*********************************************** + ******** Image Data Extraction Methods ******** + ***********************************************/ + +/** + * An extension method to get the image data for the given {@link page}. + * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method + * @param page - A reference to the {@link Page} containing the necessary information to acquire the image data + * @param priority - The importance level for ordering the request for the image data within the internal task pool + * @param signal - An abort signal that can be used to cancel the request for the image data + * @param detectMimeType - Force a fingerprint check of the image data to detect its mime-type (instead of relying on the Content-Type header) + */ +export async function FetchImageAjax(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType = false): Promise { + switch (true) { + case page.Link.href.endsWith('ptimg.json'): + return await descramble_v016061(this, page, priority, signal, detectMimeType); + case page.Link.href.includes('sbcGetImg'): + case page.Link.href.includes('M_L.jpg'): + case page.Link.href.includes('M_H.jpg'): + case page.Link.href.includes('/img/'): + return await descramble_v016130(this, page, priority, signal, detectMimeType); + } + throw new Error('Unsupported version of SpeedBinb reader!'); +} + +async function descramble_v016061(scraper: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType = false): Promise { + const data = await FetchJSON(new Request(page.Link)); + const fakepage = new Page(scraper, page.Parent as Chapter, new URL(data.resources.i.src, page.Link.href)); + const imagedata: Blob = await Common.FetchImageAjax.call(scraper, fakepage, priority, signal, detectMimeType); + + return DeScramble(imagedata, async (image, ctx) => { + const view = data.views[0]; + + for (const part of view.coords) { + const num = part.split(/[:,+>]/); + const sourceX = parseInt(num[1]); + const sourceY = parseInt(num[2]); + const targetX = parseInt(num[5]); + const targetY = parseInt(num[6]); + const partWidth = parseInt(num[3]); + const partHeight = parseInt(num[4]); + ctx.drawImage(image, sourceX, sourceY, partWidth, partHeight, targetX, targetY, partWidth, partHeight); + } + }); +} + +async function descramble_v016130(scraper: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType: boolean): Promise { + const imagedata: Blob = await Common.FetchImageAjax.call(scraper, page, priority, signal, detectMimeType); + const descrambleKeyPair: DescrambleKP = JSON.parse(window.atob(page.Link.hash.slice(1))); + + return DeScramble(imagedata, async (image, ctx) => { + const view = getImageDescrambleCoords(descrambleKeyPair.s, descrambleKeyPair.u, image.width, image.height); + for (const part of view.transfers[0].coords) { + ctx.drawImage(image, part.xsrc, part.ysrc, part.width, part.height, part.xdest, part.ydest, part.width, part.height); + } + }); +} + +/** + * A class decorator that adds the ability to get the image data for a given page by loading the source asynchronous with the `Fetch API`. + * @param detectMimeType - Force a fingerprint check of the image data to detect its mime-type (instead of relying on the Content-Type header) + */ +export function ImageAjax(detectMimeType = false) { + return function DecorateClass(ctor: T, context?: ClassDecoratorContext): T { + Common.ThrowOnUnsupportedDecoratorContext(context); + + return class extends ctor { + public async FetchImage(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal): Promise { + return FetchImageAjax.call(this, page, priority, signal, detectMimeType); + } + }; + }; +} + +function _tt(t: string): string { + const n = Date.now().toString(16).padStart(16, 'x'); // w.getRandomString(16) + const i = Array(Math.ceil(16 / t.length) + 1).join(t); + const r = i.substring(0, 16); + const e = i.substring(i.length - 16); + /* + const r = i.substr(0, 16); + const e = i.substr(-16, 16); + */ + let s = 0; + let u = 0; + let h = 0; + return n.split("").map(function (t, i) { + return s ^= n.charCodeAt(i), + u ^= r.charCodeAt(i), + h ^= e.charCodeAt(i), + t + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"[s + u + h & 63]; + }).join(""); +} + +function _pt(t: string, i: string, n: string): string[] { + const r = t + ':' + i; + let e = 0; + + for (let s = 0; s < r.length; s++) { + e += r.charCodeAt(s) << s % 16; + } + + 0 == (e &= 2147483647) && (e = 305419896); + + let u = ''; + let h = e; + + for (let s = 0; s < n.length; s++) { + h = h >>> 1 ^ 1210056708 & -(1 & h); + const o = (n.charCodeAt(s) - 32 + h) % 94 + 32; + u += String.fromCharCode(o); + } + try { + return JSON.parse(u); + } catch { + return null; + } +} + +/** + * Determine which descramble key pair from ctbl / ptbl shall be used + * depending on the given image name 'pages/cu77gvXE.jpg' + */ +function lt_001(t: string, ctbl: string[], ptbl: string[]): DescrambleKP { + const i = [0, 0]; + const n = t.lastIndexOf("/") + 1; + const r = t.length - n; + if (t) { + for (let e = 0; e < r; e++) + i[e % 2] += t.charCodeAt(e + n); + i[0] %= 8, + i[1] %= 8; + } + return { s: ptbl[i[0]], u: ctbl[i[1]] }; +} + +/** + * Copied from official SpeedBinb library + * t imagecontext containing src property ('pages/cu77gvXE.jpg') + * s, u descramble key pair, used to determine descrambler object + * i width of descrambled image + * n height of descrambled image + */ +function getImageDescrambleCoords(/*t*/s: string, u: string, i: number, n: number): PageViewv016130 { + const r = _lt_002(s, u); // var r = this.lt(t.src); + if (!r || !r.vt()) + return null; + const e = r.dt({ + width: i, + height: n + }); + return { + width: e.width, + height: e.height, + transfers: [{ + index: 0, + coords: r.gt({ + width: i, + height: n + }) + }] + }; +} + +/** + * Get a descrambler based on the descramble key pair from ctbl / ptbl + */ +function _lt_002(s: string, u: string) { + return "=" === u.charAt(0) && "=" === s.charAt(0) ? new _speedbinb_f(u, s) : u.match(/^[0-9]/) && s.match(/^[0-9]/) ? new _speedbinb_a(u, s) : "" === u && "" === s ? new _speedbinb_h : null; +} + +/** + * Copied from official SpeedBinb library + * define prototype for f + */ +const _speedbinb_f = function () { + function s(t: string, i: string) { + this.Mt = null; + const n = t.match(/^=([0-9]+)-([0-9]+)([-+])([0-9]+)-([-_0-9A-Za-z]+)$/), + r = i.match(/^=([0-9]+)-([0-9]+)([-+])([0-9]+)-([-_0-9A-Za-z]+)$/); + + if (null !== n && null !== r && n[1] === r[1] && n[2] === r[2] && n[4] === r[4] && "+" === n[3] && "-" === r[3] && (this.C = parseInt(n[1], 10), + this.I = parseInt(n[2], 10), + this.jt = parseInt(n[4], 10), + !(8 < this.C || 8 < this.I || 64 < this.C * this.I))) { + const e = this.C + this.I + this.C * this.I; + if (n[5].length === e && r[5].length === e) { + const s = this.yt(n[5]), + u = this.yt(r[5]); + this.xt = s.n, + this.Et = s.t, + this.It = u.n, + this.St = u.t, + this.Mt = []; + for (let h = 0; h < this.C * this.I; h++) + this.Mt.push(s.p[u.p[h]]); + } + } + } + return s.prototype.vt = function (): boolean { + return null !== this.Mt; + } + , + s.prototype.bt = function (t: Dimensions): boolean { + const i = 2 * this.C * this.jt, + n = 2 * this.I * this.jt; + return t.width >= 64 + i && t.height >= 64 + n && t.width * t.height >= (320 + i) * (320 + n); + } + , + s.prototype.dt = function (t: Dimensions): Dimensions { + return this.bt(t) ? { + width: t.width - 2 * this.C * this.jt, + height: t.height - 2 * this.I * this.jt + } : t; + } + , + s.prototype.gt = function (t: Dimensions): DrawImageCoords[] { + if (!this.vt()) + return null; + if (!this.bt(t)) + return [{ + xsrc: 0, + ysrc: 0, + width: t.width, + height: t.height, + xdest: 0, + ydest: 0 + }]; + + const h: DrawImageCoords[] = []; + const i = t.width - 2 * this.C * this.jt, + n = t.height - 2 * this.I * this.jt, + r = Math.floor((i + this.C - 1) / this.C), + e = i - (this.C - 1) * r, + s = Math.floor((n + this.I - 1) / this.I), + u = n - (this.I - 1) * s; + + for (let o = 0; o < this.C * this.I; ++o) { + const a = o % this.C, + f = Math.floor(o / this.C), + c = this.jt + a * (r + 2 * this.jt) + (this.It[f] < a ? e - r : 0), + l = this.jt + f * (s + 2 * this.jt) + (this.St[a] < f ? u - s : 0), + v = this.Mt[o] % this.C, + d = Math.floor(this.Mt[o] / this.C), + g = v * r + (this.xt[d] < v ? e - r : 0), + p = d * s + (this.Et[v] < d ? u - s : 0), + b = this.It[f] === a ? e : r, + m = this.St[a] === f ? u : s; + 0 < i && 0 < n && h.push({ + xsrc: c, + ysrc: l, + width: b, + height: m, + xdest: g, + ydest: p + }); + } + return h; + } + , + s.prototype.yt = function (t: string) { + let i; + const n = [], r = [], e = []; + for (i = 0; i < this.C; i++) + n.push(s.Tt[t.charCodeAt(i)]); + for (i = 0; i < this.I; i++) + r.push(s.Tt[t.charCodeAt(this.C + i)]); + for (i = 0; i < this.C * this.I; i++) + e.push(s.Tt[t.charCodeAt(this.C + this.I + i)]); + return { + t: n, + n: r, + p: e + }; + } + , + s.Tt = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1], + s; +}(); + +/** + * Copied from official SpeedBinb library + * define prototype for a + */ +const _speedbinb_a = function () { + function t(t: string, i: string) { + this.mt = null, + this.wt = null; + const n = this.yt(t); + const r = this.yt(i); + n && r && n.ndx === r.ndx && n.ndy === r.ndy && (this.mt = n, + this.wt = r); + } + return t.prototype.vt = function (): boolean { + return null !== this.mt && null !== this.wt; + } + , + t.prototype.bt = function (t: Dimensions): boolean { + return 64 <= t.width && 64 <= t.height && 102400 <= t.width * t.height; + } + , + t.prototype.dt = function (t: Dimensions): Dimensions { + return t; + } + , + t.prototype.gt = function (t: Dimensions): DrawImageCoords[] { + if (!this.vt()) + return null; + const i = []; + const n = t.width - t.width % 8, + r = Math.floor((n - 1) / 7) - Math.floor((n - 1) / 7) % 8, + e = n - 7 * r, + s = t.height - t.height % 8, + u = Math.floor((s - 1) / 7) - Math.floor((s - 1) / 7) % 8, + h = s - 7 * u, + o = this.mt.piece.length; + if (!this.bt(t)) + return [{ + xsrc: 0, + ysrc: 0, + width: t.width, + height: t.height, + xdest: 0, + ydest: 0 + }]; + for (let a = 0; a < o; a++) { + const f = this.mt.piece[a], + c = this.wt.piece[a]; + i.push({ + xsrc: Math.floor(f.x / 2) * r + f.x % 2 * e, + ysrc: Math.floor(f.y / 2) * u + f.y % 2 * h, + width: Math.floor(f.w / 2) * r + f.w % 2 * e, + height: Math.floor(f.h / 2) * u + f.h % 2 * h, + xdest: Math.floor(c.x / 2) * r + c.x % 2 * e, + ydest: Math.floor(c.y / 2) * u + c.y % 2 * h + }); + } + const l = r * (this.mt.ndx - 1) + e, + v = u * (this.mt.ndy - 1) + h; + return l < t.width && i.push({ + xsrc: l, + ysrc: 0, + width: t.width - l, + height: v, + xdest: l, + ydest: 0 + }), + v < t.height && i.push({ + xsrc: 0, + ysrc: v, + width: t.width, + height: t.height - v, + xdest: 0, + ydest: v + }), + i; + } + , + t.prototype.yt = function (t: string) { + if (!t) + return null; + const i = t.split("-"); + if (3 != i.length) + return null; + const n = parseInt(i[0], 10), + r = parseInt(i[1], 10), + e = i[2]; + if (e.length != n * r * 2) + return null; + const v = []; + const a = (n - 1) * (r - 1) - 1; + const f = a + (n - 1); + const c = f + (r - 1); + const l = c + 1; + + for (let s, u, h, o, d = 0; d < n * r; d++) + s = this.Ot(e.charAt(2 * d)), + u = this.Ot(e.charAt(2 * d + 1)), + d <= a ? o = h = 2 : d <= f ? (h = 2, o = 1) : d <= c ? (h = 1, o = 2) : d <= l && (o = h = 1), + v.push({ + x: s, + y: u, + w: h, + h: o + }); + return { + ndx: n, + ndy: r, + piece: v + }; + } + , + t.prototype.Ot = function (t: string) { + let i = 0; + let n = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(t); + return n < 0 ? n = "abcdefghijklmnopqrstuvwxyz".indexOf(t) : i = 1, + i + 2 * n; + } + , + t; +}(); + +/** + * Copied from official SpeedBinb library + * define prototype for h + */ +const _speedbinb_h = function () { + function t() { } + return t.prototype.vt = function (): boolean { + return !0; + } + , + t.prototype.bt = function (): boolean { + return !1; + } + , + t.prototype.dt = function (t: Dimensions): Dimensions { + return t; + } + , + t.prototype.gt = function (t: Dimensions): DrawImageCoords[] { + return [{ + xsrc: 0, + ysrc: 0, + width: t.width, + height: t.height, + xdest: 0, + ydest: 0 + }]; + } + , + t; +}(); \ No newline at end of file diff --git a/web/src/engine/websites/legacy/BookLive.ts b/web/src/engine/websites/legacy/BookLive.ts deleted file mode 100755 index 104c854d46..0000000000 --- a/web/src/engine/websites/legacy/BookLive.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './BookLive.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('booklive', `BookLive`, 'https://booklive.jp' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class BookLive extends SpeedBinb { - - constructor() { - super(); - super.id = 'booklive'; - super.label = 'BookLive'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'https://booklive.jp'; - } - - async _getMangaFromURI(uri) { - let request = new Request(uri, this.requestOptions); - let data = await this.fetchDOM(request, 'div#product_detail_area div.product_info > h1#product_display_1', 3); - let id = uri.pathname; - let title = data[0].innerText.trim(); - return new Manga(this, id, title); - } - - async _getMangas() { - // https://booklive.jp/select/title/page_no/5012 - const msg = 'This website does not support mangas/chapters, please copy and paste the links containing the chapters directly from your browser into HakuNeko.'; - throw new Error(msg); - } - - async _getChapters(manga) { - const uri = new URL(manga.id, this.url); - const request = new Request(uri, this.requestOptions); - const data = await this.fetchDOM(request, 'div#product_detail_area div.product_actions ul a.bl-bviewer'); - return data.map(element => { - return { - id: '/bviewer/s/?cid=' + element.dataset.title + '_' + element.dataset.vol, - title: element.dataset.vol.trim(), - language: '' - }; - }); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicBrise.ts b/web/src/engine/websites/legacy/ComicBrise.ts deleted file mode 100755 index 1be1490f08..0000000000 --- a/web/src/engine/websites/legacy/ComicBrise.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './ComicBrise.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('comicbrise', `comicBrise`, 'https://www.comic-brise.com' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class ComicBrise extends SpeedBinb { - - constructor() { - super(); - super.id = 'comicbrise'; - super.label = 'comicBrise'; - this.tags = ['manga', 'japanese']; - this.url = 'https://www.comic-brise.com'; - } - - async _getMangaFromURI(uri) { - let request = new Request(uri, this.requestOptions); - let data = await this.fetchDOM(request, '.post-title'); - let id = uri.pathname; - let title = data[0].innerText.trim(); - return new Manga(this, id, title); - } - - async _getMangas() { - const request = new Request(new URL('/titlelist', this.url), this.requestOptions); - const data = await this.fetchDOM(request, ".list-works a"); - return data.map(element => { - return { - id: this.getRootRelativeOrAbsoluteLink(element.pathname, this.url), - title: element.innerText.trim() - }; - }); - } - async _getChapters(manga) { - const uri = new URL(manga.id, this.url); - const request = new Request(uri, this.requestOptions); - const data = await this.fetchDOM(request, '.modal.modal-chapter .modal-body'); - return data.reverse() - .filter(e => e.querySelector(".banner-trial source").getAttribute("alt") == "FREE") - .map(element => { - return { - id: element.querySelector('.banner-trial a').pathname, - title: element.querySelector('.primary-title').textContent.trim() - }; - }); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicMeteor.ts b/web/src/engine/websites/legacy/ComicMeteor.ts deleted file mode 100755 index a6811b8c19..0000000000 --- a/web/src/engine/websites/legacy/ComicMeteor.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './ComicMeteor.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('comicmeteor', `COMICメテオ (COMIC Meteor)`, 'https://comic-meteor.jp' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class ComicMeteor extends SpeedBinb { - - /** - * - * - constructor() { - super(); - super.id = 'comicmeteor'; - super.label = 'COMICメテオ (COMIC Meteor)'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'https://comic-meteor.jp'; - } - - /** - * - * - _getMangaListFromPages( page ) { - page = page || 1; - let request = new Request( this.url + '/wp-admin/admin-ajax.php?action=get_flex_titles_for_toppage&get_num=64&page=' + page, this.requestOptions ); - return this.fetchDOM( request, 'div.update_work_size div.update_work_info_img a', 5 ) - .then( data => { - let mangaList = data.map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, request.url ), - title: element.querySelector( 'source' ).getAttribute('alt').trim() - }; - } ); - if( mangaList.length > 0 ) { - return this._getMangaListFromPages( page + 1 ) - .then( mangas => mangaList.concat( mangas ) ); - } else { - return Promise.resolve( mangaList ); - } - } ); - } - - /** - * - * - _getMangaList( callback ) { - this._getMangaListFromPages() - .then( data => { - callback( null, data ); - } ) - .catch( error => { - console.error( error, this ); - callback( error, undefined ); - } ); - } - - /** - * - * - _getChapterList( manga, callback ) { - let request = new Request( this.url + manga.id, this.requestOptions ); - this.fetchDOM( request, 'div#contents' ) - .then( data => { - data = data[0]; - let chapterList = [...data.querySelectorAll( 'div.work_episode div.work_episode_box div.work_episode_table div.work_episode_link_btn a' )] - .map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, request.url ), - title: element.closest( 'div.work_episode_table' ).querySelector( 'div.work_episode_txt' ).innerText.replace( manga.title, '' ).trim(), - language: '' - }; - } ); - if( chapterList.length === 0 ) { - chapterList = [...data.querySelectorAll( 'div.latest_info_box div.latest_info_link_btn01 a' )] - .map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, request.url ), - title: element.text.replace( '読む', '' ).trim(), - language: '' - }; - } ); - } - callback( null, chapterList ); - } ) - .catch( error => { - console.error( error, manga ); - callback( error, undefined ); - } ); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicPolaris.ts b/web/src/engine/websites/legacy/ComicPolaris.ts deleted file mode 100755 index 931ee7bef8..0000000000 --- a/web/src/engine/websites/legacy/ComicPolaris.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './ComicPolaris.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('comicpolaris', `COMICポラリス (COMIC Polaris)`, 'https://comic-polaris.jp' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class ComicPolaris extends SpeedBinb { - - /** - * - * - constructor() { - super(); - super.id = 'comicpolaris'; - super.label = 'COMICポラリス (COMIC Polaris)'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'https://comic-polaris.jp'; - } - - /** - * - * - _getMangaListFromPages( page ) { - page = page || 1; - let request = new Request( this.url + '/wp-admin/admin-ajax.php?action=get_flex_titles_for_toppage&get_num=64&page=' + page, this.requestOptions ); - return this.fetchDOM( request, 'div.update_work_size div.update_work_info_img a', 5 ) - .then( data => { - let mangaList = data.map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, request.url ), - title: element.querySelector( 'source' ).getAttribute('alt').trim() - }; - } ); - if( mangaList.length > 0 ) { - return this._getMangaListFromPages( page + 1 ) - .then( mangas => mangaList.concat( mangas ) ); - } else { - return Promise.resolve( mangaList ); - } - } ); - } - - /** - * - * - _getMangaList( callback ) { - this._getMangaListFromPages() - .then( data => { - callback( null, data ); - } ) - .catch( error => { - console.error( error, this ); - callback( error, undefined ); - } ); - } - - /** - * - * - _getChapterList( manga, callback ) { - let request = new Request( this.url + manga.id, this.requestOptions ); - this.fetchDOM( request, 'div#contents' ) - .then( data => { - data = data[0]; - let chapterList = [...data.querySelectorAll( 'div.work_episode div.work_episode_box div.work_episode_table div.work_episode_link_btn a' )] - .map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, request.url ), - title: element.closest( 'div.work_episode_table' ).querySelector( 'div.work_episode_txt' ).innerText.replace( manga.title, '' ).trim(), - language: '' - }; - } ); - if( chapterList.length === 0 ) { - chapterList = [...data.querySelectorAll( 'div.latest_info_box div.latest_info_link_btn01 a' )] - .map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, request.url ), - title: element.text.replace( '読む', '' ).trim(), - language: '' - }; - } ); - } - callback( null, chapterList ); - } ) - .catch( error => { - console.error( error, manga ); - callback( error, undefined ); - } ); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/ComicPolaris.webp b/web/src/engine/websites/legacy/ComicPolaris.webp deleted file mode 100644 index 4382357e09..0000000000 Binary files a/web/src/engine/websites/legacy/ComicPolaris.webp and /dev/null differ diff --git a/web/src/engine/websites/legacy/ComicValkyrie.ts b/web/src/engine/websites/legacy/ComicValkyrie.ts deleted file mode 100755 index 6ac0617b4e..0000000000 --- a/web/src/engine/websites/legacy/ComicValkyrie.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './ComicValkyrie.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('comicvalkyrie', `Comic Valkyrie`, 'https://www.comic-valkyrie.com' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class ComicValkyrie extends SpeedBinb { - - constructor() { - super(); - super.id = 'comicvalkyrie'; - super.label = 'Comic Valkyrie'; - this.tags = ['manga', 'japanese']; - this.url = 'https://www.comic-valkyrie.com'; - } - - async _getChapters(manga) { - const uri = new URL(manga.id, this.url); - const request = new Request(uri, this.requestOptions); - const data = await this.fetchDOM(request, '#new_story .title, #back_number .title'); - return data.map(element => { - const a = element.parentElement.querySelector('a.read_bt'); - return { - id: a.href, - title: element.textContent, - language: '' - }; - }); - } - - async _getMangaFromURI(uri) { - let request = new Request(uri, this.requestOptions); - let data = await this.fetchDOM(request, 'meta[property="og:title"]', 3); - let id = uri.pathname.slice(1); - let title = this.cleanMangaTitle(data[0].content); - return new Manga(this, id, title); - } - - async _getMangas() { - let request = new Request(this.url + '/list', this.requestOptions); - let data = await this.fetchDOM(request, '.box_wrap .box'); - return data.map(element => ({ - id: new URL(element.querySelector('a').href).pathname.replace('/new.html', '').slice(1), - title: this.cleanMangaTitle(element.querySelector('.title').textContent), - })); - } - - cleanMangaTitle(str) { - return str.replace(/\s*THE COMIC\s*i, '').trim(); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/Futabanet.ts b/web/src/engine/websites/legacy/Futabanet.ts deleted file mode 100755 index 9a78c233f2..0000000000 --- a/web/src/engine/websites/legacy/Futabanet.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './Futabanet.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('futabanet', `がうがうモンスター (Futabanet Monster)`, 'https://gaugau.futabanet.jp' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class Futabanet extends SpeedBinb { - - constructor() { - super(); - super.id = 'futabanet'; - super.label = 'がうがうモンスター (Futabanet Monster)'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'https://gaugau.futabanet.jp'; - } - - async _getMangaFromURI(uri) { - let request = new Request(uri, this.requestOptions); - let data = await this.fetchDOM(request, 'h1.detail-ex__title'); - let id = uri.pathname; - let title = data[0].textContent.trim(); - return new Manga(this, id, title); - } - - async _getMangas() { - let request = new Request(new URL('/list/works', this.url), this.requestOptions); - let pages = await this.fetchDOM(request, 'li.m-pager__last a'); - pages = Number( new URL(pages[0].href).searchParams.get('page') ); - - let data; - let mangas = []; - for (let page = 1; page <= pages; page++) { - request = new Request(this.url + '/list/works?page=' + page); - data = await this.fetchDOM(request, 'div.m-result-list__item a'); - mangas.push( ...data.map(element => { - return { - id: this.getRootRelativeOrAbsoluteLink(element, this.url), - title: element.querySelector('.m-result-list__title').textContent.trim() - }; - })); - } - return mangas; - } - - async _getChapters(manga) { - let request = new Request(new URL(manga.id, this.url), this.requestOptions); - let data = await this.fetchDOM(request, 'section.detail-sec.detail-ex div.detail-ex__btn-item a'); - return data.map(element => { - let title = element.querySelector('span:not(.new)'); - return { - id: this.getRootRelativeOrAbsoluteLink(element, request.url), - title: title.innerText.replace(/\(\d+\.\d+(.*)\)$/, '').trim(), - }; - }); - } - -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/Ohtabooks.ts b/web/src/engine/websites/legacy/Ohtabooks.ts deleted file mode 100755 index 25785a0c2e..0000000000 --- a/web/src/engine/websites/legacy/Ohtabooks.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './Ohtabooks.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('ohtabooks', `Ohtabooks`, 'https://webcomic.ohtabooks.com' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class Ohtabooks extends SpeedBinb { - - /** - * - * - constructor() { - super(); - super.id = 'ohtabooks'; - super.label = 'Ohtabooks'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'http://webcomic.ohtabooks.com'; - } - - /** - * - * - _getMangaList( callback ) { - let uri = this.url + '/list/'; - this.fetchDOM( uri, 'div.bnrList ul li a' ) - .then( data => { - let mangaList = data.map( element => { - return { - id: this.getRootRelativeOrAbsoluteLink( element, uri ), - title: element.querySelector( '.title' ).textContent.trim() - }; - } ); - callback( null, mangaList ); - } ) - .catch( error => { - console.error( error, this ); - callback( error, undefined ); - } ); - } - - /** - * - * - _getChapterList( manga, callback ) { - let uri = this.url + manga.id; - this.fetchDOM( uri, 'a[onClick^="return !openBook("]' ) - .then( data => { - let chapterList = data.map( element => { - let partId = element.getAttribute( 'onclick' ); - partId = partId.match(/\d+/); - - let title = element.querySelector( '.title' ); - if( title ) { - title = title.textContent; - } else if( element.classList.contains( 'btnMini' ) ) { - title = element.textContent; - } - - return { - id: this.getRootRelativeOrAbsoluteLink( 'https://yondemill.jp/contents/' + partId + '?view=1&u0=1', uri ), - title: title ? title.trim() : 'マンガをよむ', - language: 'ja' - }; - } ); - - // Remove duplicates - chapterList = chapterList.reverse().filter( ( chapter, index ) => { - return index === chapterList.findIndex( c => c.id === chapter.id ); - } ); - - callback( null, chapterList ); - } ) - .catch( error => { - console.error( error, manga ); - callback( error, undefined ); - } ); - } - - _getPageList( manga, chapter, callback ) { - this.fetchDOM( chapter.id, 'script[type="text/javascript"]' ) - .then( data => { - data = data[0].innerHTML; - let ch = { - id: data.substring( data.indexOf('\'') + 1, data.lastIndexOf('\'') ), - title: chapter.title, - language: chapter.language - }; - super._getPageList( manga, ch, callback ); - } ); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/OneTwoThreeHon.ts b/web/src/engine/websites/legacy/OneTwoThreeHon.ts deleted file mode 100755 index 76ed40c243..0000000000 --- a/web/src/engine/websites/legacy/OneTwoThreeHon.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './OneTwoThreeHon.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('onetwothreehon', `123hon`, 'https://www.123hon.com' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class OneTwoThreeHon extends SpeedBinb { - - constructor() { - super(); - super.id = 'onetwothreehon'; - super.label = '123hon'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'https://www.123hon.com'; - this.mangaList = '/polca/web-comic/'; - - this.queryMangas = 'ul.comic__list > li > a'; - this.queryChapters = 'div.read-episode li'; - } - - async _getMangas() { - const request = new Request(new URL(this.mangaList, this.url), this.requestOptions); - const data = await this.fetchDOM(request, this.queryMangas); - return data.map(link => { - return { - id: this.getRootRelativeOrAbsoluteLink(link, this.url), - title: link.href.match(/(\w+)\/?$/)[1] // Rather crude, but there are no text titles on the listing - }; - }); - } - - async _getChapters(manga) { - const request = new Request(new URL(manga.id, this.url), this.requestOptions); - const data = await this.fetchDOM(request, this.queryChapters); - - return data.map(element => { - if (element.querySelector('a')) { // otherwise chapter not available - return { - id: element.querySelector('a').href, - title: element.innerText.match(/\s*(.*?)\s+/)[1] - }; - } - }).filter(element => element !== undefined); - } -} -*/ \ No newline at end of file diff --git a/web/src/engine/websites/legacy/YoungJump.ts b/web/src/engine/websites/legacy/YoungJump.ts deleted file mode 100755 index f90da2a8c5..0000000000 --- a/web/src/engine/websites/legacy/YoungJump.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Auto-Generated export from HakuNeko Legacy -// See: https://gist.github.com/ronny1982/0c8d5d4f0bd9c1f1b21dbf9a2ffbfec9 - -//import { Tags } from '../../Tags'; -import icon from './YoungJump.webp'; -import { DecoratableMangaScraper } from '../../providers/MangaPlugin'; - -export default class extends DecoratableMangaScraper { - - public constructor() { - super('youngjump', `ヤングジャンプ / ウルトラジャンプ (young jump/ultra jump)`, 'https://www.youngjump.world' /*, Tags.Language.English, Tags ... */); - } - - public override get Icon() { - return icon; - } -} - -// Original Source -/* -class YoungJump extends SpeedBinb { - - constructor() { - super(); - super.id = 'youngjump'; - super.label = 'ヤングジャンプ / ウルトラジャンプ (young jump/ultra jump)'; - this.tags = [ 'manga', 'japanese' ]; - this.url = 'https://www.youngjump.world'; - this.links = { - login: 'https://www.youngjump.world/?login=1' - }; - } - - async _getMangas() { - return [ - { - id: 'free_uj/', - title: 'ウルトラジャンプ - ultra jump' - }, - { - id: 'free_yj/', - title: 'ヤングジャンプ - young jump' - } - ]; - } - - async _getChapters(manga) { - const request = new Request(new URL(manga.id, this.url), this.requestOptions); - const data = await this.fetchDOM(request); - - let chapters = []; - for (const year of data.querySelectorAll('section.sp-w')) { - const mangas = [...year.querySelectorAll('a.p-my__list-link')]; - chapters.push(...mangas.map(element => { - return { - id: this.getAbsolutePath(element.href, this.url), - title: year.querySelector('h3').innerText.trim() +' - ' + element.querySelector('h4').innerText.trim() - }; - })); - } - - return chapters; - } - -} -*/ \ No newline at end of file