From e8263965f572939576210333a549d749069e1ca7 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Dec 2023 17:40:37 +0100
Subject: [PATCH 01/31] add HeanCMS decorator and update YugenMangaES

---
 web/src/engine/websites/YugenMangasES.ts      |  15 +-
 web/src/engine/websites/decorators/HeanCMS.ts | 276 ++++++++++++++++++
 2 files changed, 283 insertions(+), 8 deletions(-)
 create mode 100644 web/src/engine/websites/decorators/HeanCMS.ts

diff --git a/web/src/engine/websites/YugenMangasES.ts b/web/src/engine/websites/YugenMangasES.ts
index f3b1374639..1d7fc5d686 100755
--- a/web/src/engine/websites/YugenMangasES.ts
+++ b/web/src/engine/websites/YugenMangasES.ts
@@ -1,16 +1,15 @@
 import { Tags } from '../Tags';
 import icon from './YugenMangasES.webp';
 import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as Madara from './decorators/WordPressMadara';
-import * as Common from './decorators/Common';
+import * as HeamCMS from './decorators/HeanCMS';
 
-//TODO: needs to be recoded with novel support && Yugenmanga API
+const apiUrl = 'https://api.yugenmangas.net';
 
-@Madara.MangaCSS(/^{origin}\/series\/[^/]+\/$/)
-@Madara.MangasMultiPageAJAX()
-@Madara.ChaptersSinglePageAJAXv2()
-@Madara.PagesSinglePageCSS()
-@Common.ImageAjax()
+@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
+@HeamCMS.MangasMultiPageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.PagesSinglePageAJAX(apiUrl)
+@HeamCMS.ImageAjax()
 export default class extends DecoratableMangaScraper {
 
     public constructor() {
diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
new file mode 100644
index 0000000000..213b36aa67
--- /dev/null
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -0,0 +1,276 @@
+import { FetchRequest, FetchJSON, FetchWindowScript } from '../../FetchProvider';
+import { type MangaScraper, type MangaPlugin, Manga, Chapter, Page } from '../../providers/MangaPlugin';
+import type { Priority } from '../../taskpool/TaskPool';
+import * as Common from './Common';
+
+//TODO: get novel color theme from settings and apply them to the script somehow. Setting must ofc have been initializated before
+//add a listener on the aforementionned setting
+
+const DefaultNovelScript = `
+            new Promise((resolve, reject) => {
+                document.body.style.width = '56em';
+                let container = document.querySelector('div.container');
+                container.style.maxWidth = '56em';
+                container.style.padding = '0';
+                container.style.margin = '0';
+	            let novel = document.querySelector('div#reader-container');
+                novel.style.padding = '1.5em';
+                [...novel.querySelectorAll(":not(:empty)")].forEach(ele => {
+                    ele.style.backgroundColor = 'black'
+                    ele.style.color = 'white'
+                })
+                novel.style.backgroundColor = 'black'
+                novel.style.color = 'white'
+                let script = document.createElement('script');
+                script.onerror = error => reject(error);
+                script.onload = async function() {
+                    try {
+                        let canvas = await html2canvas(novel);
+                        resolve([canvas.toDataURL('image/png')]);
+                    } catch (error){
+                        reject(error)
+                    }
+                }
+                script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
+                document.body.appendChild(script);
+            });
+ `;
+
+type APIManga = {
+    title: string
+    series_type: 'Comic' | 'Novel',
+    series_slug: string,
+    seasons? : APISeason[]
+}
+
+type APIResult<T> ={
+    data : T[]
+}
+
+type APISeason = {
+    index: number,
+    chapters: APIChapter[]
+}
+
+type APIChapter = {
+    index: string,
+    chapter_name: string,
+    chapter_title: string,
+    chapter_slug: string,
+}
+
+type APIPages = {
+    chapter_type: 'Comic' | 'Novel',
+    paywall: boolean,
+    data: string[] | string
+}
+
+/***************************************************
+ ******** Manga from URL Extraction Methods ********
+ ***************************************************/
+
+/**
+ * An extension method for extracting a single manga from the given {@link url} using the HeanCMS api url {@link apiUrl}.
+ * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
+ * @param provider - A reference to the {@link MangaPlugin} which shall be assigned as parent for the extracted manga
+ * @param url - the manga url
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export async function FetchMangaCSS(this: MangaScraper, provider: MangaPlugin, url: string, apiUrl: string): Promise<Manga> {
+    const slug = new URL(url).pathname.split('/')[2];
+    const request = new FetchRequest(new URL(`/series/${slug}`, apiUrl).href);
+    const { title, series_slug } = await FetchJSON<APIManga>(request);
+    return new Manga(this, provider, series_slug, title);
+}
+
+/**
+ * An extension method for extracting a single manga from any url using the HeanCMS api url {@link apiUrl}.
+ * @param pattern - An expression to check if a manga can be extracted from an url or not, it may contain the placeholders `{origin}` and `{hostname}` which will be replaced with the corresponding parameters based on the website's base URL
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export function MangaCSS(pattern: RegExp, apiURL: string) {
+    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
+        Common.ThrowOnUnsupportedDecoratorContext(context);
+        return class extends ctor {
+            public ValidateMangaURL(this: MangaScraper, url: string): boolean {
+                const source = pattern.source.replaceAll('{origin}', this.URI.origin).replaceAll('{hostname}', this.URI.hostname);
+                return new RegExp(source, pattern.flags).test(url);
+            }
+            public async FetchManga(this: MangaScraper, provider: MangaPlugin, url: string): Promise<Manga> {
+                return FetchMangaCSS.call(this, provider, url, apiURL);
+            }
+        };
+    };
+}
+
+/***********************************************
+ ******** Manga List Extraction Methods ********
+ ***********************************************/
+
+/**
+ * An extension method for extracting multiple mangas using the HeanCMS api url {@link apiUrl}.
+ * The range begins with 1 and is incremented until no more new mangas can be extracted.
+ * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
+ * @param provider - A reference to the {@link MangaPlugin} which shall be assigned as parent for the extracted mangas
+ * @param apiUrl - The url of the HeanCMS api for the website
+ * @param throttle - A delay [ms] for each request (only required for rate-limited websites)
+ */
+export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: MangaPlugin, apiUrl: string, throttle = 0): Promise<Manga[]> {
+    const mangaList: Manga[] = [];
+    for (let page = 1, run = true; run; page++) {
+        const mangas = await getMangaFromPage.call(this, provider, page, apiUrl);
+        mangas.length > 0 ? mangaList.push(...mangas) : run = false;
+        await new Promise(resolve => setTimeout(resolve, throttle));
+    }
+    return mangaList;
+}
+
+async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string): Promise<Manga[]> {
+    const request = new FetchRequest(new URL(`/query?series_type=All&order=asc&perPage=100&page=${page}`, apiUrl).href);
+    const { data } = await FetchJSON<APIResult<APIManga>>(request);
+    if (data.length) {
+        return data.map((manga) => new Manga(this, provider, manga.series_slug, manga.title));
+    }
+    return [];
+}
+
+/**
+ * A class decorator that adds the ability to extract multiple mangas from a range of pages using the HeanCMS api url {@link apiUrl}.
+ * @param apiUrl - The url of the HeanCMS api for the website
+ * @param throttle - A delay [ms] for each request (only required for rate-limited websites)
+ */
+export function MangasMultiPageAJAX(apiUrl : string, throttle = 0) {
+    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
+        Common.ThrowOnUnsupportedDecoratorContext(context);
+        return class extends ctor {
+            public async FetchMangas(this: MangaScraper, provider: MangaPlugin): Promise<Manga[]> {
+                return FetchMangasMultiPageAJAX.call(this, provider, apiUrl, throttle);
+            }
+        };
+    };
+}
+
+/*************************************************
+ ******** Chapter List Extraction Methods ********
+ *************************************************/
+
+/**
+ * An extension method for extracting all chapters for the given {@link manga} using the HeanCMS api url {@link apiUrl}.
+ * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
+ * @param manga - A reference to the {@link Manga} which shall be assigned as parent for the extracted chapters
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export async function FetchChaptersSinglePageAJAX(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
+    const request = new FetchRequest(new URL(`/series/${manga.Identifier}`, apiUrl).href);
+    const { seasons } = await FetchJSON<APIManga>(request);
+    const chapterList: Chapter[] = [];
+
+    seasons.map((season) => season.chapters.map((chapter) => {
+        const id = JSON.stringify({
+            series: manga.Identifier,
+            chapter: chapter.chapter_slug
+        });
+        const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
+        chapterList.push(new Chapter(this, manga, id, title));
+    }));
+    return chapterList;
+}
+
+/**
+ * A class decorator that adds the ability to extract all chapters for a given manga from this website using the HeanCMS api url {@link apiUrl}.
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export function ChaptersSinglePageAJAX(apiUrl: string) {
+    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
+        Common.ThrowOnUnsupportedDecoratorContext(context);
+        return class extends ctor {
+            public async FetchChapters(this: MangaScraper, manga: Manga): Promise<Chapter[]> {
+                return FetchChaptersSinglePageAJAX.call(this, manga, apiUrl);
+            }
+        };
+    };
+}
+
+/**********************************************
+ ******** Page List Extraction Methods ********
+ **********************************************/
+/**
+ * An extension method for extracting all pages for the given {@link chapter} using the HeanCMS api url {@link apiUrl}.
+ * @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 apiUrl - The url of the HeanCMS api for the website
+ */
+export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string ): Promise<Page[]> {
+    const id = JSON.parse(chapter.Identifier);
+    const request = new FetchRequest(new URL(`/chapter/${id.series}/${id.chapter}`, apiUrl).href);
+    const { chapter_type, data, paywall } = await FetchJSON<APIPages>(request);
+
+    // check for paywall
+    if (data.length < 1 && paywall) {
+        throw new Error(`${chapter.Title} is paywalled. Please login.`); //localize this
+    }
+
+    // check if novel
+    if (chapter_type.toLowerCase() === 'novel') {
+        return [new Page(this, chapter, new URL(`/series/${id.series}/${id.chapter}`, this.URI), { type: chapter_type })];
+    }
+
+    return (data as string[]).map(image => new Page(this, chapter, new URL(image), { type: chapter_type }));
+}
+
+/**
+ * A class decorator that adds the ability to extract all pages for a given chapter using the HeanCMS api url {@link apiUrl}.
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export function PagesSinglePageAJAX(apiUrl: string) {
+    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
+        Common.ThrowOnUnsupportedDecoratorContext(context);
+        return class extends ctor {
+            public async FetchPages(this: MangaScraper, chapter: Chapter): Promise<Page[]> {
+                return FetchPagesSinglePageAJAX.call(this, chapter, apiUrl);
+            }
+        };
+    };
+}
+
+/***********************************************
+ ******** Image Data Extraction Methods ********
+ ***********************************************/
+
+/**
+ * An extension method to get the image data for the given {@link page} according to an XHR based-approach.
+ * @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)
+ * @param deProxifyLink - Remove common image proxies (default false)
+ * @param novelScript  - a custom script to get and transform the novel text into a dataURL
+ */
+export async function FetchImageAjax(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType = false, deProxifyLink = true, novelScript = DefaultNovelScript): Promise<Blob> {
+    if (page.Parameters?.type as string === 'Comic') {
+        return Common.FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
+    } else {
+        //TODO: test if used want to export the NOVEL as HTML?
+
+        const request = new FetchRequest(page.Link.href);
+        const data = await FetchWindowScript<string>(request, novelScript, 500, 10000);
+        return Common.FetchImageAjax.call(this, new Page(this, page.Parent as Chapter, new URL(data)), priority, signal, false, false);
+    }
+}
+
+/**
+ * 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)
+ * @param deProxifyLink - Remove common image proxies (default false)
+ */
+export function ImageAjax(detectMimeType = false, deProxifyLink = true, novelScript: string = DefaultNovelScript) {
+    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
+        Common.ThrowOnUnsupportedDecoratorContext(context);
+        return class extends ctor {
+            public async FetchImage(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal): Promise<Blob> {
+                return FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink, novelScript);
+            }
+        };
+    };
+}
\ No newline at end of file

From 2b679f91659d5ee52868c3f3b5c52c67348b27b6 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Dec 2023 18:15:00 +0100
Subject: [PATCH 02/31] small fixes and add tests

---
 web/src/engine/websites/YugenMangasES.ts      |  2 +-
 web/src/engine/websites/YugenMangasES_e2e.ts  | 46 ++++++++++++++-----
 web/src/engine/websites/decorators/HeanCMS.ts | 12 ++---
 3 files changed, 40 insertions(+), 20 deletions(-)

diff --git a/web/src/engine/websites/YugenMangasES.ts b/web/src/engine/websites/YugenMangasES.ts
index 1d7fc5d686..ec61d4c42d 100755
--- a/web/src/engine/websites/YugenMangasES.ts
+++ b/web/src/engine/websites/YugenMangasES.ts
@@ -9,7 +9,7 @@ const apiUrl = 'https://api.yugenmangas.net';
 @HeamCMS.MangasMultiPageAJAX(apiUrl)
 @HeamCMS.ChaptersSinglePageAJAX(apiUrl)
 @HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax()
+@HeamCMS.ImageAjax(true)
 export default class extends DecoratableMangaScraper {
 
     public constructor() {
diff --git a/web/src/engine/websites/YugenMangasES_e2e.ts b/web/src/engine/websites/YugenMangasES_e2e.ts
index cdba38882e..28faa8fe52 100755
--- a/web/src/engine/websites/YugenMangasES_e2e.ts
+++ b/web/src/engine/websites/YugenMangasES_e2e.ts
@@ -1,25 +1,49 @@
-import { TestFixture } from '../../../test/WebsitesFixture';
+import { TestFixture } from '../../../test/WebsitesFixture';
 
-const config = {
+const ComicConfig = {
     plugin: {
         id: 'yugenmangas-es',
         title: 'YugenMangas (ES)'
     },
     container: {
-        url: 'https://yugenmangas.com/series/demon-king-cheat-system/',
-        id: JSON.stringify({ post: '18638', slug: '/series/demon-king-cheat-system/' }),
-        title: 'Demon King Cheat System'
+        url: 'https://yugenmangas.lat/series/la-gran-duquesa-del-norte-era-una-villana-en-secreto',
+        id: 'la-gran-duquesa-del-norte-era-una-villana-en-secreto',
+        title: 'La Gran Duquesa Del Norte Era Una Villana En Secreto'
     },
     child: {
-        id: '/series/demon-king-cheat-system/capitulo/',
-        title: 'Capitulo'
+        id: 'capitulo-102',
+        title: 'Capitulo 102 FINAL'
     },
     entry: {
-        index: 0,
-        size: 772_248,
+        index: 1,
+        size: 3_064_669,
         type: 'image/jpeg'
     }
 };
 
-const fixture = new TestFixture(config);
-describe(fixture.Name, () => fixture.AssertWebsite());
\ No newline at end of file
+const ComicFixture = new TestFixture(ComicConfig);
+describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
+
+const NovelConfig = {
+    plugin: {
+        id: 'yugenmangas-es',
+        title: 'YugenMangas (ES)'
+    },
+    container: {
+        url: 'https://yugenmangas.lat/series/esposo-villano-deberias-estar-obsesionado-con-esa-persona-',
+        id: 'esposo-villano-deberias-estar-obsesionado-con-esa-persona-',
+        title: 'Esposo villano, deberías estar obsesionado con esa persona.'
+    },
+    child: {
+        id: 'capitulo-1',
+        title: 'Capítulo 1'
+    },
+    entry: {
+        index: 0,
+        size: 556_399,
+        type: 'image/png'
+    }
+};
+
+const NovelFixture = new TestFixture(NovelConfig);
+describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index 213b36aa67..3c6de2a437 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -166,10 +166,7 @@ export async function FetchChaptersSinglePageAJAX(this: MangaScraper, manga: Man
     const chapterList: Chapter[] = [];
 
     seasons.map((season) => season.chapters.map((chapter) => {
-        const id = JSON.stringify({
-            series: manga.Identifier,
-            chapter: chapter.chapter_slug
-        });
+        const id = chapter.chapter_slug;
         const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
         chapterList.push(new Chapter(this, manga, id, title));
     }));
@@ -201,8 +198,7 @@ export function ChaptersSinglePageAJAX(apiUrl: string) {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string ): Promise<Page[]> {
-    const id = JSON.parse(chapter.Identifier);
-    const request = new FetchRequest(new URL(`/chapter/${id.series}/${id.chapter}`, apiUrl).href);
+    const request = new FetchRequest(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl).href);
     const { chapter_type, data, paywall } = await FetchJSON<APIPages>(request);
 
     // check for paywall
@@ -212,7 +208,7 @@ export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chap
 
     // check if novel
     if (chapter_type.toLowerCase() === 'novel') {
-        return [new Page(this, chapter, new URL(`/series/${id.series}/${id.chapter}`, this.URI), { type: chapter_type })];
+        return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: chapter_type })];
     }
 
     return (data as string[]).map(image => new Page(this, chapter, new URL(image), { type: chapter_type }));
@@ -254,7 +250,7 @@ export async function FetchImageAjax(this: MangaScraper, page: Page, priority: P
         //TODO: test if used want to export the NOVEL as HTML?
 
         const request = new FetchRequest(page.Link.href);
-        const data = await FetchWindowScript<string>(request, novelScript, 500, 10000);
+        const data = await FetchWindowScript<string>(request, novelScript, 1000, 10000);
         return Common.FetchImageAjax.call(this, new Page(this, page.Parent as Chapter, new URL(data)), priority, signal, false, false);
     }
 }

From e91776a57fa269721cbc4193a52e7771b2aaef50 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Dec 2023 18:42:01 +0100
Subject: [PATCH 03/31] add PerfScan

---
 web/src/engine/websites/PerfScan.ts      |  22 ++++++++++
 web/src/engine/websites/PerfScan.webp    | Bin 0 -> 2696 bytes
 web/src/engine/websites/PerfScan_e2e.ts  |  49 +++++++++++++++++++++++
 web/src/engine/websites/YugenMangasES.ts |   2 +-
 web/src/engine/websites/_index.ts        |   1 +
 5 files changed, 73 insertions(+), 1 deletion(-)
 create mode 100644 web/src/engine/websites/PerfScan.ts
 create mode 100644 web/src/engine/websites/PerfScan.webp
 create mode 100644 web/src/engine/websites/PerfScan_e2e.ts

diff --git a/web/src/engine/websites/PerfScan.ts b/web/src/engine/websites/PerfScan.ts
new file mode 100644
index 0000000000..afbf8d1fe1
--- /dev/null
+++ b/web/src/engine/websites/PerfScan.ts
@@ -0,0 +1,22 @@
+import { Tags } from '../Tags';
+import icon from './PerfScan.webp';
+import { DecoratableMangaScraper } from '../providers/MangaPlugin';
+import * as HeamCMS from './decorators/HeanCMS';
+
+const apiUrl = 'https://api.perf-scan.fr';
+
+@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
+@HeamCMS.MangasMultiPageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.PagesSinglePageAJAX(apiUrl)
+@HeamCMS.ImageAjax()
+export default class extends DecoratableMangaScraper {
+
+    public constructor() {
+        super('perfscan', 'Perf Scan', 'https://perf-scan.fr', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Novel, Tags.Language.French, Tags.Source.Scanlator);
+    }
+
+    public override get Icon() {
+        return icon;
+    }
+}
\ No newline at end of file
diff --git a/web/src/engine/websites/PerfScan.webp b/web/src/engine/websites/PerfScan.webp
new file mode 100644
index 0000000000000000000000000000000000000000..c8da0f7c97160254eb71cb1b774231e6853489c0
GIT binary patch
literal 2696
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
CP(E7#

literal 0
HcmV?d00001

diff --git a/web/src/engine/websites/PerfScan_e2e.ts b/web/src/engine/websites/PerfScan_e2e.ts
new file mode 100644
index 0000000000..05a2b334b7
--- /dev/null
+++ b/web/src/engine/websites/PerfScan_e2e.ts
@@ -0,0 +1,49 @@
+import { TestFixture } from '../../../test/WebsitesFixture';
+
+const ComicConfig = {
+    plugin: {
+        id: 'perfscan',
+        title: 'Perf Scan'
+    },
+    container: {
+        url: 'https://perf-scan.fr/series/martial-peak-1702249200756',
+        id: 'martial-peak-1702249200756',
+        title: 'Martial Peak'
+    },
+    child: {
+        id: 'chapitre-1298',
+        title: 'S6 Chapitre 1298'
+    },
+    entry: {
+        index: 2,
+        size: 215_840,
+        type: 'image/webp'
+    }
+};
+
+const ComicFixture = new TestFixture(ComicConfig);
+describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
+
+const NovelConfig = {
+    plugin: {
+        id: 'perfscan',
+        title: 'Perf Scan'
+    },
+    container: {
+        url: 'https://perf-scan.fr/series/demonic-emperor-novel-1702249200676',
+        id: 'demonic-emperor-novel-1702249200676',
+        title: 'Demonic emperor - Novel'
+    },
+    child: {
+        id: 'chapitre-492',
+        title: 'S4 Chapitre 492'
+    },
+    entry: {
+        index: 0,
+        size: 789_130,
+        type: 'image/png'
+    }
+};
+
+const NovelFixture = new TestFixture(NovelConfig);
+describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/YugenMangasES.ts b/web/src/engine/websites/YugenMangasES.ts
index ec61d4c42d..6d07fcd83a 100755
--- a/web/src/engine/websites/YugenMangasES.ts
+++ b/web/src/engine/websites/YugenMangasES.ts
@@ -13,7 +13,7 @@ const apiUrl = 'https://api.yugenmangas.net';
 export default class extends DecoratableMangaScraper {
 
     public constructor() {
-        super('yugenmangas-es', 'YugenMangas (ES)', 'https://yugenmangas.lat', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Language.Spanish);
+        super('yugenmangas-es', 'YugenMangas (ES)', 'https://yugenmangas.lat', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Novel, Tags.Language.Spanish, Tags.Source.Aggregator);
     }
 
     public override get Icon() {
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index 9118a80ff0..68f425ae82 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -425,6 +425,7 @@ export { default as PatyScans } from './PatyScans';
 export { default as PCNet } from './PCNet';
 export { default as PelaTeam } from './PelaTeam';
 export { default as Penlab } from './Penlab';
+export { default as PerfScan } from './PerfScan';
 export { default as PhoenixScansIT } from './PhoenixScansIT';
 export { default as Piccoma } from './Piccoma';
 export { default as PiccomaFR } from './PiccomaFR';

From a7aeaf643c3206df0511f4fcf58402f53d2acc26 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Dec 2023 22:22:37 +0100
Subject: [PATCH 04/31] add OmegaScans

---
 web/src/engine/websites/OmegaScans.ts         |  22 ++++++++
 web/src/engine/websites/OmegaScans.webp       | Bin 0 -> 2362 bytes
 web/src/engine/websites/OmegaScans_e2e.ts     |  49 ++++++++++++++++++
 web/src/engine/websites/_index.ts             |   1 +
 web/src/engine/websites/decorators/HeanCMS.ts |   2 +-
 5 files changed, 73 insertions(+), 1 deletion(-)
 create mode 100644 web/src/engine/websites/OmegaScans.ts
 create mode 100644 web/src/engine/websites/OmegaScans.webp
 create mode 100644 web/src/engine/websites/OmegaScans_e2e.ts

diff --git a/web/src/engine/websites/OmegaScans.ts b/web/src/engine/websites/OmegaScans.ts
new file mode 100644
index 0000000000..c944d06f25
--- /dev/null
+++ b/web/src/engine/websites/OmegaScans.ts
@@ -0,0 +1,22 @@
+import { Tags } from '../Tags';
+import icon from './OmegaScans.webp';
+import { DecoratableMangaScraper } from '../providers/MangaPlugin';
+import * as HeamCMS from './decorators/HeanCMS';
+
+const apiUrl = 'https://api.omegascans.org';
+
+@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
+@HeamCMS.MangasMultiPageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.PagesSinglePageAJAX(apiUrl)
+@HeamCMS.ImageAjax(true)
+export default class extends DecoratableMangaScraper {
+
+    public constructor() {
+        super('omegascans', 'OmegaScans', 'https://omegascans.org', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Novel, Tags.Language.English, Tags.Source.Scanlator, Tags.Rating.Pornographic);
+    }
+
+    public override get Icon() {
+        return icon;
+    }
+}
\ No newline at end of file
diff --git a/web/src/engine/websites/OmegaScans.webp b/web/src/engine/websites/OmegaScans.webp
new file mode 100644
index 0000000000000000000000000000000000000000..4a9338b49051dab4fba3ac4d9c558eb81fc7f6fe
GIT binary patch
literal 2362
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

literal 0
HcmV?d00001

diff --git a/web/src/engine/websites/OmegaScans_e2e.ts b/web/src/engine/websites/OmegaScans_e2e.ts
new file mode 100644
index 0000000000..3b71966b5c
--- /dev/null
+++ b/web/src/engine/websites/OmegaScans_e2e.ts
@@ -0,0 +1,49 @@
+import { TestFixture } from '../../../test/WebsitesFixture';
+
+const ComicConfig = {
+    plugin: {
+        id: 'omegascans',
+        title: 'OmegaScans'
+    },
+    container: {
+        url: 'https://omegascans.org/series/trapped-in-the-academys-eroge',
+        id: 'trapped-in-the-academys-eroge',
+        title: `Trapped in the Academy's Eroge`
+    },
+    child: {
+        id: 'chapter-76',
+        title: 'Chapter 76'
+    },
+    entry: {
+        index: 1,
+        size: 1_577_008,
+        type: 'image/jpeg'
+    }
+};
+
+const ComicFixture = new TestFixture(ComicConfig);
+describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
+
+const NovelConfig = {
+    plugin: {
+        id: 'omegascans',
+        title: 'OmegaScans'
+    },
+    container: {
+        url: 'https://omegascans.org/series/the-scion-of-the-labyrinth-city',
+        id: 'the-scion-of-the-labyrinth-city',
+        title: 'The Scion of the Labyrinth City'
+    },
+    child: {
+        id: 'chapter-28',
+        title: 'Chapter 28'
+    },
+    entry: {
+        index: 0,
+        size: 892_312,
+        type: 'image/png'
+    }
+};
+
+const NovelFixture = new TestFixture(NovelConfig);
+describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index 68f425ae82..0835f5259a 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -415,6 +415,7 @@ export { default as NoraNoFansub } from './NoraNoFansub';
 export { default as Noromax } from './Noromax';
 export { default as NovelMic } from './NovelMic';
 export { default as OlympusScanlation } from './OlympusScanlation';
+export { default as OmegaScans } from './OmegaScans';
 export { default as OnMangaMe } from './OnMangaMe';
 export { default as Opiatoon } from './Opiatoon';
 export { default as Oremanga } from './Oremanga';
diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index 3c6de2a437..f706933207 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -247,7 +247,7 @@ export async function FetchImageAjax(this: MangaScraper, page: Page, priority: P
     if (page.Parameters?.type as string === 'Comic') {
         return Common.FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
     } else {
-        //TODO: test if used want to export the NOVEL as HTML?
+        //TODO: test if user want to export the NOVEL as HTML?
 
         const request = new FetchRequest(page.Link.href);
         const data = await FetchWindowScript<string>(request, novelScript, 1000, 10000);

From 237c012a1c3402a3193e04146d30c46bbdc2ca82 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Dec 2023 22:34:52 +0100
Subject: [PATCH 05/31] add TempleScan

---
 web/src/engine/websites/TempleScan.ts     |  22 +++++++++++++++++++
 web/src/engine/websites/TempleScan.webp   | Bin 0 -> 1950 bytes
 web/src/engine/websites/TempleScan_e2e.ts |  25 ++++++++++++++++++++++
 web/src/engine/websites/_index.ts         |   1 +
 4 files changed, 48 insertions(+)
 create mode 100644 web/src/engine/websites/TempleScan.ts
 create mode 100644 web/src/engine/websites/TempleScan.webp
 create mode 100644 web/src/engine/websites/TempleScan_e2e.ts

diff --git a/web/src/engine/websites/TempleScan.ts b/web/src/engine/websites/TempleScan.ts
new file mode 100644
index 0000000000..bf1f86ab88
--- /dev/null
+++ b/web/src/engine/websites/TempleScan.ts
@@ -0,0 +1,22 @@
+import { Tags } from '../Tags';
+import icon from './TempleScan.webp';
+import { DecoratableMangaScraper } from '../providers/MangaPlugin';
+import * as HeamCMS from './decorators/HeanCMS';
+
+const apiUrl = 'https://api.templescan.net';
+
+@HeamCMS.MangaCSS(/^{origin}\/comic\/[^/]+$/, apiUrl)
+@HeamCMS.MangasMultiPageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.PagesSinglePageAJAX(apiUrl)
+@HeamCMS.ImageAjax(true)
+export default class extends DecoratableMangaScraper {
+
+    public constructor() {
+        super('templescan', 'TempleScan', 'https://templescan.net', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Language.English, Tags.Source.Scanlator);
+    }
+
+    public override get Icon() {
+        return icon;
+    }
+}
\ No newline at end of file
diff --git a/web/src/engine/websites/TempleScan.webp b/web/src/engine/websites/TempleScan.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a85135682e061a2f4dfc9605cb86a1bd38cfa1cf
GIT binary patch
literal 1950
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

literal 0
HcmV?d00001

diff --git a/web/src/engine/websites/TempleScan_e2e.ts b/web/src/engine/websites/TempleScan_e2e.ts
new file mode 100644
index 0000000000..260f9f0294
--- /dev/null
+++ b/web/src/engine/websites/TempleScan_e2e.ts
@@ -0,0 +1,25 @@
+import { TestFixture } from '../../../test/WebsitesFixture';
+
+const ComicConfig = {
+    plugin: {
+        id: 'templescan',
+        title: 'TempleScan'
+    },
+    container: {
+        url: 'https://templescan.net/comic/predatory-marriage-complete-edition',
+        id: 'predatory-marriage-complete-edition',
+        title: `Predatory Marriage (Complete Edition)`
+    },
+    child: {
+        id: 'chapter-15',
+        title: 'Chapter 15'
+    },
+    entry: {
+        index: 0,
+        size: 2_953_394,
+        type: 'image/jpeg'
+    }
+};
+
+const ComicFixture = new TestFixture(ComicConfig);
+describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index 0835f5259a..577837081f 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -520,6 +520,7 @@ export { default as TCBScans } from './TCBScans';
 export { default as Team1x1 } from './Team1x1';
 export { default as TecnoScan } from './TecnoScan';
 export { default as Tempestfansub } from './Tempestfansub';
+export { default as TempleScan } from './TempleScan';
 export { default as TenshiID } from './TenshiID';
 export { default as TheGuildScans } from './TheGuildScans';
 export { default as TonariNoYoungJump } from './TonariNoYoungJump';

From 460c12394b5d85217f878d5c9d2b5869b4566cb0 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Fri, 5 Jan 2024 17:18:13 +0100
Subject: [PATCH 06/31] Update _index.ts

---
 web/src/engine/websites/_index.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index 010135bb09..eae9c9b7ea 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -418,6 +418,7 @@ export { default as NoraNoFansub } from './NoraNoFansub';
 export { default as Noromax } from './Noromax';
 export { default as NovelMic } from './NovelMic';
 export { default as OlympusScanlation } from './OlympusScanlation';
+export { default as OmegaScans } from './OmegaScans';
 export { default as OnMangaMe } from './OnMangaMe';
 export { default as Opiatoon } from './Opiatoon';
 export { default as Oremanga } from './Oremanga';
@@ -429,6 +430,7 @@ export { default as PCNet } from './PCNet';
 export { default as PeanuToon } from './PeanuToon';
 export { default as PelaTeam } from './PelaTeam';
 export { default as Penlab } from './Penlab';
+export { default as PerfScan } from './PerfScan';
 export { default as PhenixScans } from './PhenixScans';
 export { default as PhoenixScansIT } from './PhoenixScansIT';
 export { default as Piccoma } from './Piccoma';
@@ -534,6 +536,7 @@ export { default as Team1x1 } from './Team1x1';
 export { default as TecnoScan } from './TecnoScan';
 export { default as Tempestfansub } from './Tempestfansub';
 export { default as TempestScans } from './TempestScans';
+export { default as TempleScan } from './TempleScan';
 export { default as TenshiID } from './TenshiID';
 export { default as TheGuildScans } from './TheGuildScans';
 export { default as TitanManga } from './TitanManga';

From b12ee87b0746640c8e5d44f74598c415a013d6ec Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sat, 6 Jan 2024 14:51:28 +0100
Subject: [PATCH 07/31] HeanCMS : fix pictures CDN

---
 web/src/engine/websites/decorators/HeanCMS.ts | 37 ++++++++++++++-----
 1 file changed, 27 insertions(+), 10 deletions(-)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index f706933207..e68291cf74 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -40,11 +40,11 @@ type APIManga = {
     title: string
     series_type: 'Comic' | 'Novel',
     series_slug: string,
-    seasons? : APISeason[]
+    seasons?: APISeason[]
 }
 
-type APIResult<T> ={
-    data : T[]
+type APIResult<T> = {
+    data: T[]
 }
 
 type APISeason = {
@@ -63,6 +63,9 @@ type APIPages = {
     chapter_type: 'Comic' | 'Novel',
     paywall: boolean,
     data: string[] | string
+    chapter: {
+        storage: string
+    }
 }
 
 /***************************************************
@@ -139,7 +142,7 @@ async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page:
  * @param apiUrl - The url of the HeanCMS api for the website
  * @param throttle - A delay [ms] for each request (only required for rate-limited websites)
  */
-export function MangasMultiPageAJAX(apiUrl : string, throttle = 0) {
+export function MangasMultiPageAJAX(apiUrl: string, throttle = 0) {
     return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
         Common.ThrowOnUnsupportedDecoratorContext(context);
         return class extends ctor {
@@ -197,21 +200,23 @@ export function ChaptersSinglePageAJAX(apiUrl: string) {
  * @param chapter - A reference to the {@link Chapter} which shall be assigned as parent for the extracted pages
  * @param apiUrl - The url of the HeanCMS api for the website
  */
-export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string ): Promise<Page[]> {
+export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page[]> {
     const request = new FetchRequest(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl).href);
-    const { chapter_type, data, paywall } = await FetchJSON<APIPages>(request);
+    const { chapter_type, data, paywall, chapter: { storage } } = await FetchJSON<APIPages>(request);
 
-    // check for paywall
-    if (data.length < 1 && paywall) {
+    if (paywall) {
         throw new Error(`${chapter.Title} is paywalled. Please login.`); //localize this
     }
 
+    //in case of novel data is the html string, in case of comic its an array of strings (pictures urls or pathnames)
+    if (!data || data.length < 1) return [];
+
     // check if novel
     if (chapter_type.toLowerCase() === 'novel') {
         return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: chapter_type })];
     }
 
-    return (data as string[]).map(image => new Page(this, chapter, new URL(image), { type: chapter_type }));
+    return (data as string[]).map(image => new Page(this, chapter, computePageUrl(image, storage, apiUrl), { type: chapter_type }));
 }
 
 /**
@@ -269,4 +274,16 @@ export function ImageAjax(detectMimeType = false, deProxifyLink = true, novelScr
             }
         };
     };
-}
\ No newline at end of file
+}
+/**
+ * Compute the image full URL for HeanHMS based websites
+ * @param image - A string containing the full url ( for {@link storage} "s3") or the pathname ( for {@link storage} "local"))
+ * @param storage - As string representing the type of storage used : "s3" or "local"
+ * @param apiUrl - The url of the HeanCMS api for the website *
+ */
+function computePageUrl(image: string, storage: string, apiUrl: string): URL {
+    switch (storage) {
+        case "s3": return new URL(image);
+        case "local": return new URL(image, apiUrl);
+    }
+}

From 10913f13a8a591d38aaed85673a13b4b9befc4c3 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Tue, 16 Jan 2024 13:55:41 +0100
Subject: [PATCH 08/31] improve platform abstraction

---
 web/src/engine/websites/decorators/HeanCMS.ts | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index e68291cf74..156d9ab4c7 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -1,4 +1,4 @@
-import { FetchRequest, FetchJSON, FetchWindowScript } from '../../FetchProvider';
+import { FetchJSON, FetchWindowScript } from '../../platform/FetchProvider';
 import { type MangaScraper, type MangaPlugin, Manga, Chapter, Page } from '../../providers/MangaPlugin';
 import type { Priority } from '../../taskpool/TaskPool';
 import * as Common from './Common';
@@ -81,7 +81,7 @@ type APIPages = {
  */
 export async function FetchMangaCSS(this: MangaScraper, provider: MangaPlugin, url: string, apiUrl: string): Promise<Manga> {
     const slug = new URL(url).pathname.split('/')[2];
-    const request = new FetchRequest(new URL(`/series/${slug}`, apiUrl).href);
+    const request = new Request(new URL(`/series/${slug}`, apiUrl).href);
     const { title, series_slug } = await FetchJSON<APIManga>(request);
     return new Manga(this, provider, series_slug, title);
 }
@@ -129,7 +129,7 @@ export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: Man
 }
 
 async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string): Promise<Manga[]> {
-    const request = new FetchRequest(new URL(`/query?series_type=All&order=asc&perPage=100&page=${page}`, apiUrl).href);
+    const request = new Request(new URL(`/query?series_type=All&order=asc&perPage=100&page=${page}`, apiUrl).href);
     const { data } = await FetchJSON<APIResult<APIManga>>(request);
     if (data.length) {
         return data.map((manga) => new Manga(this, provider, manga.series_slug, manga.title));
@@ -164,7 +164,7 @@ export function MangasMultiPageAJAX(apiUrl: string, throttle = 0) {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchChaptersSinglePageAJAX(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
-    const request = new FetchRequest(new URL(`/series/${manga.Identifier}`, apiUrl).href);
+    const request = new Request(new URL(`/series/${manga.Identifier}`, apiUrl).href);
     const { seasons } = await FetchJSON<APIManga>(request);
     const chapterList: Chapter[] = [];
 
@@ -201,7 +201,7 @@ export function ChaptersSinglePageAJAX(apiUrl: string) {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page[]> {
-    const request = new FetchRequest(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl).href);
+    const request = new Request(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl).href);
     const { chapter_type, data, paywall, chapter: { storage } } = await FetchJSON<APIPages>(request);
 
     if (paywall) {
@@ -254,7 +254,7 @@ export async function FetchImageAjax(this: MangaScraper, page: Page, priority: P
     } else {
         //TODO: test if user want to export the NOVEL as HTML?
 
-        const request = new FetchRequest(page.Link.href);
+        const request = new Request(page.Link.href);
         const data = await FetchWindowScript<string>(request, novelScript, 1000, 10000);
         return Common.FetchImageAjax.call(this, new Page(this, page.Parent as Chapter, new URL(data)), priority, signal, false, false);
     }

From 389cb58109ca0c67a637fa6fef5a0bb460621b72 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 4 Feb 2024 14:12:03 +0100
Subject: [PATCH 09/31] remove YugenMangasES

its dead
---
 web/src/engine/websites/YugenMangasES.ts     |  22 ---------
 web/src/engine/websites/YugenMangasES.webp   | Bin 1516 -> 0 bytes
 web/src/engine/websites/YugenMangasES_e2e.ts |  49 -------------------
 web/src/engine/websites/_index.ts            |   1 -
 4 files changed, 72 deletions(-)
 delete mode 100755 web/src/engine/websites/YugenMangasES.ts
 delete mode 100644 web/src/engine/websites/YugenMangasES.webp
 delete mode 100755 web/src/engine/websites/YugenMangasES_e2e.ts

diff --git a/web/src/engine/websites/YugenMangasES.ts b/web/src/engine/websites/YugenMangasES.ts
deleted file mode 100755
index 6d07fcd83a..0000000000
--- a/web/src/engine/websites/YugenMangasES.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Tags } from '../Tags';
-import icon from './YugenMangasES.webp';
-import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as HeamCMS from './decorators/HeanCMS';
-
-const apiUrl = 'https://api.yugenmangas.net';
-
-@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
-@HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
-@HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax(true)
-export default class extends DecoratableMangaScraper {
-
-    public constructor() {
-        super('yugenmangas-es', 'YugenMangas (ES)', 'https://yugenmangas.lat', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Novel, Tags.Language.Spanish, Tags.Source.Aggregator);
-    }
-
-    public override get Icon() {
-        return icon;
-    }
-}
\ No newline at end of file
diff --git a/web/src/engine/websites/YugenMangasES.webp b/web/src/engine/websites/YugenMangasES.webp
deleted file mode 100644
index abdf02c9e72a36dfff52e8483298aa427a09e8dd..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1516
zcmWIYbaQ*c%D@or>J$(bU=hK^z`$St#P(q1>FgXJ!35+oFiC(&7NAsaUP)1qyOTmh
zWRwC(3<eUDiwj_EEHonn!?$Y;K$3ysvMv^RLPDG=sl~}aMIV5;qzGs^0|R3UkS&r9
zVON0INf34qh+PyC<P21^0La!zM`9--vCB&eN`PukfYc`!6oJ@xfNY!GoKlcoAOLbP
zPikIhFp!P_Vi|^Fh9Cxa1}7k!ssIt5`HL7BEWR=@Fi${;CCy}D*xt&(AaM&Jrm}#6
zfxm@;VcYqF#G*v7V_1MBQ(77W!>8p847`yH48j)}7`UQg4n^oi#03t&DggD!0R5Q{
zgy{@945<tX44w>m49N_7K$a0Okr*<VIr;>6<TC?pTHum>ThX&hX;aSX+}qo7Z`*Er
z8wsR9$Xc}f|L@N(DeKe11PvMg<eO-Qd_M5K@6WyX%k@XCjp`bn9Zr0I;^P^Sm9I`N
zXOxsWZTY_IT9C`yt&KHbS3b!vI%0T?e}aUCUIUZu#N`4l?Zv8|p?&)<wVZL>_q*Dl
z;+om?MW^0fsOifoRo8ImpD1AxcItYtarXn8Ddvu=b{}55<C&8E4?~YTv!%Q_EBUnN
zD@NCHT)XJ3!q&(9>B~hOv!=BCi54q=O6jk&JmGJznsNW#3iYLGbD!F#thRTwc~pDy
zg=uTS*;(NSx9oQ5acFP-XDRFZ^k+58=ht^jm(GdV{c%@=k;rA%AGM6FOw+E5x$#cz
z=__8la&M!q`Ol}e3GS~Qx^j#kI3`wao8Bw-!uIN{t2dv7#`okg-1KMt#NY70)pkAK
zwFz7{yQ}Zc?eABZzI2Ig;p-x?;2e3=kRsFQG~d@-KkJmZ_HtD(`pxQY9@;4)VK7fJ
z_jl7pXM@eL*Oy!qw<+6e6XdH^`u5}2{Dw0bjY;aJ@k+nmCS3{8+;WXATl0e0&O45p
z+h(WyZ+j)k)O?Id;;)Hnzo&EIub-V-K~E3oH%=+>eYbj=(8tDO`O@6Cyx03QEXxR<
zziaBd4BKy_uKPNyA~y1LaMkm))UZde^-a4h6Cm`7iC<2PN%^?x0@eeUp6RKdxic%)
ztF|H3vYT18ulc9-gTK$K4{Xxab14pyT+^c+&Zkk$k;Sy;+!?k#TXoepJ(m**7OE(_
zr^7tOrtKsDga2m2?%r8)vmJOFeD9p#f3hoWDi>?n!+_AUt_=rIMZ8mMbNqDL{6EvR
zaElnf&Eg#^nD*G6IKACdKyu~;X^W3eMYmI*U3$Z$#rTHtb(NpNCVti*Y=?9XY|=ir
z)RK`W<QRi`7jv&tqjow2GkdX8^*hIm+5>w<Z^SeHV0)lGM{jjyhLdE&Vus^hOb-wI
z^|k7c4K)>S{CBG2sh1w}M|m0T#W%Uvr8VR){bZmORAJ|BD6%`&(!_e9#(_xZ%HJlw
z5}i_yT=uNA<gA^tvSa!ut$a6*MfaD@Iq{BtMlR!jo0lG$sbY=$HmzLqY@ufy_l_(N
z_S1~_?lW)Ny-GE<MQ$4F=L!CzjMo-yWjMU-{4(!CgFo>KPQ`mP1(xm%V)oefa-}<y
z+#c0{`?5@5?>)6oWdN4c3VOf{I)RyCE~AzMg9C%z#f<eqE7Vk!7I82#NO5K|FbJ3j
T2e4MO?qp#2_jzsuP%#4lnJ@7}

diff --git a/web/src/engine/websites/YugenMangasES_e2e.ts b/web/src/engine/websites/YugenMangasES_e2e.ts
deleted file mode 100755
index 28faa8fe52..0000000000
--- a/web/src/engine/websites/YugenMangasES_e2e.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { TestFixture } from '../../../test/WebsitesFixture';
-
-const ComicConfig = {
-    plugin: {
-        id: 'yugenmangas-es',
-        title: 'YugenMangas (ES)'
-    },
-    container: {
-        url: 'https://yugenmangas.lat/series/la-gran-duquesa-del-norte-era-una-villana-en-secreto',
-        id: 'la-gran-duquesa-del-norte-era-una-villana-en-secreto',
-        title: 'La Gran Duquesa Del Norte Era Una Villana En Secreto'
-    },
-    child: {
-        id: 'capitulo-102',
-        title: 'Capitulo 102 FINAL'
-    },
-    entry: {
-        index: 1,
-        size: 3_064_669,
-        type: 'image/jpeg'
-    }
-};
-
-const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
-
-const NovelConfig = {
-    plugin: {
-        id: 'yugenmangas-es',
-        title: 'YugenMangas (ES)'
-    },
-    container: {
-        url: 'https://yugenmangas.lat/series/esposo-villano-deberias-estar-obsesionado-con-esa-persona-',
-        id: 'esposo-villano-deberias-estar-obsesionado-con-esa-persona-',
-        title: 'Esposo villano, deberías estar obsesionado con esa persona.'
-    },
-    child: {
-        id: 'capitulo-1',
-        title: 'Capítulo 1'
-    },
-    entry: {
-        index: 0,
-        size: 556_399,
-        type: 'image/png'
-    }
-};
-
-const NovelFixture = new TestFixture(NovelConfig);
-describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index cb1164c5f5..9deb011b4d 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -600,7 +600,6 @@ export { default as YaoiHavenReborn } from './YaoiHavenReborn';
 export { default as YaoiScan } from './YaoiScan';
 export { default as YaoiToshokan } from './YaoiToshokan';
 export { default as YawarakaSpirits } from './YawarakaSpirits';
-export { default as YugenMangasES } from './YugenMangasES';
 export { default as YugenMangasPT } from './YugenMangasPT';
 export { default as YumeKomik } from './YumeKomik';
 export { default as YuriVerso } from './YuriVerso';

From cb7e37b945e63ce8abc02a26985843f929e7dc0f Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 26 Feb 2024 16:19:12 +0100
Subject: [PATCH 10/31] Update _index.ts

---
 web/src/engine/websites/_index.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index a13256c2ed..81bc8bd342 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -478,6 +478,7 @@ export { default as NovelMic } from './NovelMic';
 export { default as NvManga } from './NvManga';
 export { default as Nyrax } from './Nyrax';
 export { default as OlympusScanlation } from './OlympusScanlation';
+export { default as OmegaScans } from './OmegaScans';
 export { default as OnMangaMe } from './OnMangaMe';
 export { default as Opiatoon } from './Opiatoon';
 export { default as Oremanga } from './Oremanga';
@@ -489,6 +490,7 @@ export { default as PCNet } from './PCNet';
 export { default as PeanuToon } from './PeanuToon';
 export { default as PelaTeam } from './PelaTeam';
 export { default as Penlab } from './Penlab';
+export { default as PerfScan } from './PerfScan';
 export { default as PhenixScans } from './PhenixScans';
 export { default as PhoenixScansIT } from './PhoenixScansIT';
 export { default as Piccoma } from './Piccoma';
@@ -606,6 +608,7 @@ export { default as Team1x1 } from './Team1x1';
 export { default as TecnoScan } from './TecnoScan';
 export { default as Tempestfansub } from './Tempestfansub';
 export { default as TempestScans } from './TempestScans';
+export { default as TempleScan } from './TempleScan';
 export { default as TenshiID } from './TenshiID';
 export { default as TheBlank } from './TheBlank';
 export { default as TheGuildScans } from './TheGuildScans';

From 8fb06112f3ee914e2cda423c53699df858402eb3 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sat, 9 Mar 2024 13:40:55 +0100
Subject: [PATCH 11/31] minor code changes

---
 web/src/engine/websites/decorators/HeanCMS.ts | 67 +++++++++----------
 1 file changed, 32 insertions(+), 35 deletions(-)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index 156d9ab4c7..e851bfe98a 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -7,33 +7,33 @@ import * as Common from './Common';
 //add a listener on the aforementionned setting
 
 const DefaultNovelScript = `
-            new Promise((resolve, reject) => {
-                document.body.style.width = '56em';
-                let container = document.querySelector('div.container');
-                container.style.maxWidth = '56em';
-                container.style.padding = '0';
-                container.style.margin = '0';
-	            let novel = document.querySelector('div#reader-container');
-                novel.style.padding = '1.5em';
-                [...novel.querySelectorAll(":not(:empty)")].forEach(ele => {
-                    ele.style.backgroundColor = 'black'
-                    ele.style.color = 'white'
-                })
-                novel.style.backgroundColor = 'black'
-                novel.style.color = 'white'
-                let script = document.createElement('script');
-                script.onerror = error => reject(error);
-                script.onload = async function() {
-                    try {
-                        let canvas = await html2canvas(novel);
-                        resolve([canvas.toDataURL('image/png')]);
-                    } catch (error){
-                        reject(error)
-                    }
-                }
-                script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
-                document.body.appendChild(script);
-            });
+    new Promise((resolve, reject) => {
+        document.body.style.width = '56em';
+        let container = document.querySelector('div.container');
+        container.style.maxWidth = '56em';
+        container.style.padding = '0';
+        container.style.margin = '0';
+        let novel = document.querySelector('div#reader-container');
+        novel.style.padding = '1.5em';
+        [...novel.querySelectorAll(":not(:empty)")].forEach(ele => {
+            ele.style.backgroundColor = 'black'
+            ele.style.color = 'white'
+        })
+        novel.style.backgroundColor = 'black'
+        novel.style.color = 'white'
+        let script = document.createElement('script');
+        script.onerror = error => reject(error);
+        script.onload = async function() {
+            try {
+                let canvas = await html2canvas(novel);
+                resolve([canvas.toDataURL('image/png')]);
+            } catch (error) {
+                reject(error)
+            }
+        }
+        script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
+        document.body.appendChild(script);
+    });
  `;
 
 type APIManga = {
@@ -81,8 +81,7 @@ type APIPages = {
  */
 export async function FetchMangaCSS(this: MangaScraper, provider: MangaPlugin, url: string, apiUrl: string): Promise<Manga> {
     const slug = new URL(url).pathname.split('/')[2];
-    const request = new Request(new URL(`/series/${slug}`, apiUrl).href);
-    const { title, series_slug } = await FetchJSON<APIManga>(request);
+    const { title, series_slug } = await FetchJSON<APIManga>(new Request(new URL(`/series/${slug}`, apiUrl)));
     return new Manga(this, provider, series_slug, title);
 }
 
@@ -129,7 +128,7 @@ export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: Man
 }
 
 async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string): Promise<Manga[]> {
-    const request = new Request(new URL(`/query?series_type=All&order=asc&perPage=100&page=${page}`, apiUrl).href);
+    const request = new Request(new URL(`/query?series_type=All&order=asc&perPage=100&page=${page}`, apiUrl));
     const { data } = await FetchJSON<APIResult<APIManga>>(request);
     if (data.length) {
         return data.map((manga) => new Manga(this, provider, manga.series_slug, manga.title));
@@ -164,7 +163,7 @@ export function MangasMultiPageAJAX(apiUrl: string, throttle = 0) {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchChaptersSinglePageAJAX(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
-    const request = new Request(new URL(`/series/${manga.Identifier}`, apiUrl).href);
+    const request = new Request(new URL(`/series/${manga.Identifier}`, apiUrl));
     const { seasons } = await FetchJSON<APIManga>(request);
     const chapterList: Chapter[] = [];
 
@@ -201,7 +200,7 @@ export function ChaptersSinglePageAJAX(apiUrl: string) {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page[]> {
-    const request = new Request(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl).href);
+    const request = new Request(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl));
     const { chapter_type, data, paywall, chapter: { storage } } = await FetchJSON<APIPages>(request);
 
     if (paywall) {
@@ -253,9 +252,7 @@ export async function FetchImageAjax(this: MangaScraper, page: Page, priority: P
         return Common.FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
     } else {
         //TODO: test if user want to export the NOVEL as HTML?
-
-        const request = new Request(page.Link.href);
-        const data = await FetchWindowScript<string>(request, novelScript, 1000, 10000);
+        const data = await FetchWindowScript<string>(new Request(page.Link), novelScript, 1000, 10000);
         return Common.FetchImageAjax.call(this, new Page(this, page.Parent as Chapter, new URL(data)), priority, signal, false, false);
     }
 }

From 712854406506b2e332faa115ca382e78bdf8796f Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Fri, 12 Apr 2024 13:50:13 +0200
Subject: [PATCH 12/31] Remove TempleScans

it doesnt use HeanCMS anymore
---
 web/src/engine/websites/TempleScan.ts     |  22 -------------------
 web/src/engine/websites/TempleScan.webp   | Bin 1950 -> 0 bytes
 web/src/engine/websites/TempleScan_e2e.ts |  25 ----------------------
 web/src/engine/websites/_index.ts         |   1 -
 4 files changed, 48 deletions(-)
 delete mode 100644 web/src/engine/websites/TempleScan.ts
 delete mode 100644 web/src/engine/websites/TempleScan.webp
 delete mode 100644 web/src/engine/websites/TempleScan_e2e.ts

diff --git a/web/src/engine/websites/TempleScan.ts b/web/src/engine/websites/TempleScan.ts
deleted file mode 100644
index bf1f86ab88..0000000000
--- a/web/src/engine/websites/TempleScan.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Tags } from '../Tags';
-import icon from './TempleScan.webp';
-import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as HeamCMS from './decorators/HeanCMS';
-
-const apiUrl = 'https://api.templescan.net';
-
-@HeamCMS.MangaCSS(/^{origin}\/comic\/[^/]+$/, apiUrl)
-@HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
-@HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax(true)
-export default class extends DecoratableMangaScraper {
-
-    public constructor() {
-        super('templescan', 'TempleScan', 'https://templescan.net', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Language.English, Tags.Source.Scanlator);
-    }
-
-    public override get Icon() {
-        return icon;
-    }
-}
\ No newline at end of file
diff --git a/web/src/engine/websites/TempleScan.webp b/web/src/engine/websites/TempleScan.webp
deleted file mode 100644
index a85135682e061a2f4dfc9605cb86a1bd38cfa1cf..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1950
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

diff --git a/web/src/engine/websites/TempleScan_e2e.ts b/web/src/engine/websites/TempleScan_e2e.ts
deleted file mode 100644
index 260f9f0294..0000000000
--- a/web/src/engine/websites/TempleScan_e2e.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { TestFixture } from '../../../test/WebsitesFixture';
-
-const ComicConfig = {
-    plugin: {
-        id: 'templescan',
-        title: 'TempleScan'
-    },
-    container: {
-        url: 'https://templescan.net/comic/predatory-marriage-complete-edition',
-        id: 'predatory-marriage-complete-edition',
-        title: `Predatory Marriage (Complete Edition)`
-    },
-    child: {
-        id: 'chapter-15',
-        title: 'Chapter 15'
-    },
-    entry: {
-        index: 0,
-        size: 2_953_394,
-        type: 'image/jpeg'
-    }
-};
-
-const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index ecc62839d3..afabee320f 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -618,7 +618,6 @@ export { default as Team1x1 } from './Team1x1';
 export { default as TecnoScan } from './TecnoScan';
 export { default as Tempestfansub } from './Tempestfansub';
 export { default as TempestScans } from './TempestScans';
-export { default as TempleScan } from './TempleScan';
 export { default as TenshiID } from './TenshiID';
 export { default as TheBlank } from './TheBlank';
 export { default as TheGuildScans } from './TheGuildScans';

From a35928c48acd0e8219160397cab5ca62985c9025 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Fri, 12 Apr 2024 13:52:25 +0200
Subject: [PATCH 13/31] Revert "Remove TempleScans"

This reverts commit 712854406506b2e332faa115ca382e78bdf8796f.
---
 web/src/engine/websites/TempleScan.ts     |  22 +++++++++++++++++++
 web/src/engine/websites/TempleScan.webp   | Bin 0 -> 1950 bytes
 web/src/engine/websites/TempleScan_e2e.ts |  25 ++++++++++++++++++++++
 web/src/engine/websites/_index.ts         |   1 +
 4 files changed, 48 insertions(+)
 create mode 100644 web/src/engine/websites/TempleScan.ts
 create mode 100644 web/src/engine/websites/TempleScan.webp
 create mode 100644 web/src/engine/websites/TempleScan_e2e.ts

diff --git a/web/src/engine/websites/TempleScan.ts b/web/src/engine/websites/TempleScan.ts
new file mode 100644
index 0000000000..bf1f86ab88
--- /dev/null
+++ b/web/src/engine/websites/TempleScan.ts
@@ -0,0 +1,22 @@
+import { Tags } from '../Tags';
+import icon from './TempleScan.webp';
+import { DecoratableMangaScraper } from '../providers/MangaPlugin';
+import * as HeamCMS from './decorators/HeanCMS';
+
+const apiUrl = 'https://api.templescan.net';
+
+@HeamCMS.MangaCSS(/^{origin}\/comic\/[^/]+$/, apiUrl)
+@HeamCMS.MangasMultiPageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.PagesSinglePageAJAX(apiUrl)
+@HeamCMS.ImageAjax(true)
+export default class extends DecoratableMangaScraper {
+
+    public constructor() {
+        super('templescan', 'TempleScan', 'https://templescan.net', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Language.English, Tags.Source.Scanlator);
+    }
+
+    public override get Icon() {
+        return icon;
+    }
+}
\ No newline at end of file
diff --git a/web/src/engine/websites/TempleScan.webp b/web/src/engine/websites/TempleScan.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a85135682e061a2f4dfc9605cb86a1bd38cfa1cf
GIT binary patch
literal 1950
zcmV;P2VwY9Nk&GN2LJ$9MM6+kP&il$0000G0000#002J#06|PpNFM_L00E$c?Vlk@
z`bR|bnW?dLv~62AHO97W+qRoepT@TBnYC?iWkvjdi(<r{_7o8e+%{4)S>`?NJ%OS@
z#z`VlNxmXQTQ1w(yHt_<Nh)MH77!{vjwr5J&2}kcX0P0w_vrPR?JMStNoiN3*uU$E
zS0cug5JakE%b_c>Z@l^PcZBf!mv3%ltr*s_qzYbIY{V64u;Bi8zZ(Ex^AB1x0GQvu
zyT718fw<V}l%xW!cD?^AqUP84ceg4)QbpS=PEqkLYi|7l4r<T;5N!XhzyvJ@zua2W
zr8ucL5sjCmYEHTIBM|TmGopamcmd$YOH*npk|LVXs8;3=U~Lc+Ml*iMtVNLr6C{&T
zANawr0K@+vB}r2FgcMX{$bBG+v7;OsxId%_2^n@uDm3XOg02uNk=A%QsSv4fF%=m4
z83GJzZ~&hTEkI!+l|1Ytz@iUY;zvW1RA`#IUjejT)4-MPL^9Y{$?!BYtir6lqm_e7
z;>frBOK4a}<ICkfiX%bP@e_D(fQL^y5(!1Mv-}9SumJp7wMmJD3EwfZ3uET*CL{=|
zk!iqi0>fn1AeHhOFmB8MuTxZkCCu&u#u8ukEND1GBeUAj8*c5!(2Vzj>s?j9m+{`u
z!S$}He}2zz;1uJ>|9-f?Z`|Lv?w=d(pIg;3-C9<4-x9cdZu61?aj9>dzBf|is8+5^
z%c(_@27U@RSq45GKr*R7%2n+qGSJt0mZ?}`*vUJdEA#GU?Ac!-3w~xd9oeu61j_CH
z`H>%n9i%k8CwE`J<LkUDC*v)!?FPm$zB!-0eoVShDf`Ck_`<;;%%^7L&Mi-Zl6xH6
zu<<LvUKG5-Kl1jTTTqcCNh**!qR-f)A82FmyV>aX^HvTXH?<;>4Ep}ASSeq<Nw;i%
zx8Iu7q-@2iMFq(a$w4F%Q*7#n)FeTINQ(cAZV^IA+qy{#A%s)i7K?7&9h+|46{H(2
z>sDhT=~g?sdDx1q$htXu#n4dQY)7}(XqPf(&Wasp-+Fa^?~2)DQrgvwTDQma-ynaH
za%qQl^{-ecrvC;609H^qAW#GV05B8)odGI906+jfX)2IKq#~i13<rEb286Z%SdV?9
zd*S@u>d)l%%nDV*|GfI2+cnP*==XT<(66PE(EmbwH}V((<6XI%7Fw<>&A{ee_b_D|
z!GQC~`71W(4&#(l#LiOBAUJBw{%5ffE5C&its`Qj65;_pzO}XFM+S^hG#1@POQ=*l
z6fv!*db;?LR&^__FVctJ_N@S?49qGLP0bMhl(r&(y=EW){{I?}=3ELI&LKPCXzSS0
zOcWM+e-XK*I6jp7D9)IH=wSEjWEbbv#+(=QxNa0gy6uz2^PEwr`n#KZ%7~|OO3X;-
z2+>cuGQ>oho%lC^2Rto>=NlLUoqNo#y54?kyjGcTAd42*Yo==fvCIn*k#)|e!AZUp
z{S_T&_noKFNxM_EE*e&4!lOZ;)_@Izj6%mxfMw(KY%-yMebiLBa*i}vl2ikr3NC=F
zPumI@&PhxYs5p^uTrQYQ@t^L;QrDS#p*R9gf~eSvP|Y5>s@m4aPUE!5H%BW?KPt`}
zo&I4Qa`puJDy|zWmx?o5-YArHjRAl9KQ}Kt2i)0pR7g5hP<kT`pxE3C;8KZuPhvv0
zKYr!4CTdFxH(a0#Wh-s`DNW{0PumE*CXW-p<d4<!eKR}@F0&<Xq<%XE4sumn^!+pw
zOV)P5CmB%YuWH6riXmmGDyC~kMRB1#^>*+p)3T%c`}K-^z$!B3AQ_8;pY|M{JpepQ
zSX<Vm&7AgU<Nrz!Fo=P6;3^K(uDfODDL=SCsgxY*En*Btglf?`_>CT|H4NE*gf_Sz
zeKg6i$qrmNlb=hi?sfZ|u&K;1iGCQ^-85&Fib1nzh1N?cjCX_kK*JGLZUX?+le#Uo
z@HAKUeOU=ic;k5pUwdlLuN$D8pA(skLeQzp+?+j1IZ5b1aSDL%YsABtQv6bAqMm1Z
z<Et`)7$V;rLptbCJ`fQskg--nd$spJG3m5o!iB&2kFVt`E35gTGp8x}3c?dU-Ph?-
zhvm-DOTUtIt&+jaZXKos*K=vwHJ!gxw5t~><xTl){Gu15AlV!p4EUld<f%L?`hH;I
zoV}hGeVG*yXfnzf`x;|cHFCq#Ox>$3ore|rusLlTTHVyT8OB&k_%+801*K;NefITt
z)nT{?6mbdvi4$s9sV>np&<Axf=^$3`e5fu#qCNU<&dVsGO*_J-N}ebNs=uF&<2l}N
zK@E2Tbzlza3v>Uu>>uU84k_a>#}iblJ;~Vokcu3TW<~d?5&t{v&=Zxm+3&UdbQ9BL
z=w8m--}^CznEIdPdgIz^7cV1<lE4HG6<|=h4DR=e1|axh0T;~Y_&ulsTnalM@fwm$
z<x*&?>G*ZH`<VwO1Z<j`g)s_y_yGm+rDW4nsDNCE!<_ViELza1jT0s;byUN%CcO(Z
kDP2EvC-S-iSGmt5>&>ycGw6tmZ1&vs8v`S3Xh%AL00b1adH?_b

literal 0
HcmV?d00001

diff --git a/web/src/engine/websites/TempleScan_e2e.ts b/web/src/engine/websites/TempleScan_e2e.ts
new file mode 100644
index 0000000000..260f9f0294
--- /dev/null
+++ b/web/src/engine/websites/TempleScan_e2e.ts
@@ -0,0 +1,25 @@
+import { TestFixture } from '../../../test/WebsitesFixture';
+
+const ComicConfig = {
+    plugin: {
+        id: 'templescan',
+        title: 'TempleScan'
+    },
+    container: {
+        url: 'https://templescan.net/comic/predatory-marriage-complete-edition',
+        id: 'predatory-marriage-complete-edition',
+        title: `Predatory Marriage (Complete Edition)`
+    },
+    child: {
+        id: 'chapter-15',
+        title: 'Chapter 15'
+    },
+    entry: {
+        index: 0,
+        size: 2_953_394,
+        type: 'image/jpeg'
+    }
+};
+
+const ComicFixture = new TestFixture(ComicConfig);
+describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index afabee320f..ecc62839d3 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -618,6 +618,7 @@ export { default as Team1x1 } from './Team1x1';
 export { default as TecnoScan } from './TecnoScan';
 export { default as Tempestfansub } from './Tempestfansub';
 export { default as TempestScans } from './TempestScans';
+export { default as TempleScan } from './TempleScan';
 export { default as TenshiID } from './TenshiID';
 export { default as TheBlank } from './TheBlank';
 export { default as TheGuildScans } from './TheGuildScans';

From 5796316933b4314f8f666ea5a30c00cc1ac79fca Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sat, 13 Apr 2024 23:27:25 +0200
Subject: [PATCH 14/31] handle v2 api

---
 web/src/engine/websites/OmegaScans.ts         |   2 +-
 web/src/engine/websites/OmegaScans_e2e.ts     |   8 +-
 web/src/engine/websites/PerfScan.ts           |   2 +-
 web/src/engine/websites/PerfScan_e2e.ts       |  22 +--
 web/src/engine/websites/TempleScan.ts         |   4 +-
 web/src/engine/websites/TempleScan_e2e.ts     |   4 +-
 web/src/engine/websites/decorators/HeanCMS.ts | 133 ++++++++++++++----
 7 files changed, 125 insertions(+), 50 deletions(-)

diff --git a/web/src/engine/websites/OmegaScans.ts b/web/src/engine/websites/OmegaScans.ts
index c944d06f25..6bd81f03c7 100644
--- a/web/src/engine/websites/OmegaScans.ts
+++ b/web/src/engine/websites/OmegaScans.ts
@@ -7,7 +7,7 @@ const apiUrl = 'https://api.omegascans.org';
 
 @HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
 @HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAXv2(apiUrl)
 @HeamCMS.PagesSinglePageAJAX(apiUrl)
 @HeamCMS.ImageAjax(true)
 export default class extends DecoratableMangaScraper {
diff --git a/web/src/engine/websites/OmegaScans_e2e.ts b/web/src/engine/websites/OmegaScans_e2e.ts
index 3b71966b5c..b404947985 100644
--- a/web/src/engine/websites/OmegaScans_e2e.ts
+++ b/web/src/engine/websites/OmegaScans_e2e.ts
@@ -7,11 +7,11 @@ const ComicConfig = {
     },
     container: {
         url: 'https://omegascans.org/series/trapped-in-the-academys-eroge',
-        id: 'trapped-in-the-academys-eroge',
+        id: JSON.stringify({ id: '8', slug: 'trapped-in-the-academys-eroge' }),
         title: `Trapped in the Academy's Eroge`
     },
     child: {
-        id: 'chapter-76',
+        id: JSON.stringify({ id: '3245', slug: 'chapter-76' }),
         title: 'Chapter 76'
     },
     entry: {
@@ -31,11 +31,11 @@ const NovelConfig = {
     },
     container: {
         url: 'https://omegascans.org/series/the-scion-of-the-labyrinth-city',
-        id: 'the-scion-of-the-labyrinth-city',
+        id: JSON.stringify({ id: '186', slug: 'the-scion-of-the-labyrinth-city' }),
         title: 'The Scion of the Labyrinth City'
     },
     child: {
-        id: 'chapter-28',
+        id: JSON.stringify({ id: '3226', slug: 'chapter-28' }),
         title: 'Chapter 28'
     },
     entry: {
diff --git a/web/src/engine/websites/PerfScan.ts b/web/src/engine/websites/PerfScan.ts
index afbf8d1fe1..f988ad90fa 100644
--- a/web/src/engine/websites/PerfScan.ts
+++ b/web/src/engine/websites/PerfScan.ts
@@ -7,7 +7,7 @@ const apiUrl = 'https://api.perf-scan.fr';
 
 @HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
 @HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAXv2(apiUrl)
 @HeamCMS.PagesSinglePageAJAX(apiUrl)
 @HeamCMS.ImageAjax()
 export default class extends DecoratableMangaScraper {
diff --git a/web/src/engine/websites/PerfScan_e2e.ts b/web/src/engine/websites/PerfScan_e2e.ts
index 05a2b334b7..c3c945daaa 100644
--- a/web/src/engine/websites/PerfScan_e2e.ts
+++ b/web/src/engine/websites/PerfScan_e2e.ts
@@ -6,18 +6,18 @@ const ComicConfig = {
         title: 'Perf Scan'
     },
     container: {
-        url: 'https://perf-scan.fr/series/martial-peak-1702249200756',
-        id: 'martial-peak-1702249200756',
+        url: 'https://perf-scan.fr/series/martial-peak',
+        id: JSON.stringify({ id: '1', slug: 'martial-peak' }),
         title: 'Martial Peak'
     },
     child: {
-        id: 'chapitre-1298',
-        title: 'S6 Chapitre 1298'
+        id: JSON.stringify({ id: '28768', slug: 'chapitre-1298' }),
+        title: 'Chapitre 1298'
     },
     entry: {
         index: 2,
-        size: 215_840,
-        type: 'image/webp'
+        size: 939_807,
+        type: 'image/jpg'
     }
 };
 
@@ -30,13 +30,13 @@ const NovelConfig = {
         title: 'Perf Scan'
     },
     container: {
-        url: 'https://perf-scan.fr/series/demonic-emperor-novel-1702249200676',
-        id: 'demonic-emperor-novel-1702249200676',
+        url: 'https://perf-scan.fr/series/demonic-emperor-novel',
+        id: JSON.stringify({ id: '17', slug: 'demonic-emperor-novel' }),
         title: 'Demonic emperor - Novel'
     },
     child: {
-        id: 'chapitre-492',
-        title: 'S4 Chapitre 492'
+        id: JSON.stringify({ id: '28668', slug: 'chapitre-492' }),
+        title: 'Chapitre 492'
     },
     entry: {
         index: 0,
@@ -46,4 +46,4 @@ const NovelConfig = {
 };
 
 const NovelFixture = new TestFixture(NovelConfig);
-describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
\ No newline at end of file
+describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
diff --git a/web/src/engine/websites/TempleScan.ts b/web/src/engine/websites/TempleScan.ts
index bf1f86ab88..7efdd73540 100644
--- a/web/src/engine/websites/TempleScan.ts
+++ b/web/src/engine/websites/TempleScan.ts
@@ -3,11 +3,11 @@ import icon from './TempleScan.webp';
 import { DecoratableMangaScraper } from '../providers/MangaPlugin';
 import * as HeamCMS from './decorators/HeanCMS';
 
-const apiUrl = 'https://api.templescan.net';
+const apiUrl = 'https://templescan.net/apiv1';
 
 @HeamCMS.MangaCSS(/^{origin}\/comic\/[^/]+$/, apiUrl)
 @HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAXv1(apiUrl)
 @HeamCMS.PagesSinglePageAJAX(apiUrl)
 @HeamCMS.ImageAjax(true)
 export default class extends DecoratableMangaScraper {
diff --git a/web/src/engine/websites/TempleScan_e2e.ts b/web/src/engine/websites/TempleScan_e2e.ts
index 260f9f0294..f13bd8e16c 100644
--- a/web/src/engine/websites/TempleScan_e2e.ts
+++ b/web/src/engine/websites/TempleScan_e2e.ts
@@ -7,11 +7,11 @@ const ComicConfig = {
     },
     container: {
         url: 'https://templescan.net/comic/predatory-marriage-complete-edition',
-        id: 'predatory-marriage-complete-edition',
+        id: JSON.stringify({ id: '55', slug: 'predatory-marriage-complete-edition' }),
         title: `Predatory Marriage (Complete Edition)`
     },
     child: {
-        id: 'chapter-15',
+        id: JSON.stringify({ id: '1028', slug: '83662-chapter-15' }),
         title: 'Chapter 15'
     },
     entry: {
diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index e851bfe98a..650e46ba81 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -36,15 +36,16 @@ const DefaultNovelScript = `
     });
  `;
 
-type APIManga = {
+type APIMangaV1 = {
     title: string
+    id: number,
     series_type: 'Comic' | 'Novel',
     series_slug: string,
     seasons?: APISeason[]
 }
 
 type APIResult<T> = {
-    data: T[]
+    data: T
 }
 
 type APISeason = {
@@ -54,6 +55,7 @@ type APISeason = {
 
 type APIChapter = {
     index: string,
+    id: number,
     chapter_name: string,
     chapter_title: string,
     chapter_slug: string,
@@ -64,10 +66,20 @@ type APIPages = {
     paywall: boolean,
     data: string[] | string
     chapter: {
-        storage: string
+        chapter_type: 'Comic' | 'Novel',
+        storage: string,
+        chapter_data?: {
+            images: string[]
+        }
+
     }
 }
 
+type MangaOrChapterId = {
+    id: string,
+    slug: string
+}
+
 /***************************************************
  ******** Manga from URL Extraction Methods ********
  ***************************************************/
@@ -81,8 +93,11 @@ type APIPages = {
  */
 export async function FetchMangaCSS(this: MangaScraper, provider: MangaPlugin, url: string, apiUrl: string): Promise<Manga> {
     const slug = new URL(url).pathname.split('/')[2];
-    const { title, series_slug } = await FetchJSON<APIManga>(new Request(new URL(`/series/${slug}`, apiUrl)));
-    return new Manga(this, provider, series_slug, title);
+    const { title, series_slug, id } = await FetchJSON<APIMangaV1>(new Request(new URL(`${apiUrl}/series/${slug}`)));
+    return new Manga(this, provider, JSON.stringify({
+        id: id.toString(),
+        slug: series_slug
+    }), title);
 }
 
 /**
@@ -119,19 +134,25 @@ export function MangaCSS(pattern: RegExp, apiURL: string) {
  */
 export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: MangaPlugin, apiUrl: string, throttle = 0): Promise<Manga[]> {
     const mangaList: Manga[] = [];
+    //First loop get adult mangas
+    for (let page = 1, run = true; run; page++) {
+        const mangas = await getMangaFromPage.call(this, provider, page, apiUrl, true);
+        mangas.length > 0 ? mangaList.push(...mangas) : run = false;
+        await new Promise(resolve => setTimeout(resolve, throttle));
+    }
+    //First loop get non adult mangas
     for (let page = 1, run = true; run; page++) {
-        const mangas = await getMangaFromPage.call(this, provider, page, apiUrl);
+        const mangas = await getMangaFromPage.call(this, provider, page, apiUrl, false);
         mangas.length > 0 ? mangaList.push(...mangas) : run = false;
         await new Promise(resolve => setTimeout(resolve, throttle));
     }
-    return mangaList;
+    return mangaList.distinct();
 }
-
-async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string): Promise<Manga[]> {
-    const request = new Request(new URL(`/query?series_type=All&order=asc&perPage=100&page=${page}`, apiUrl));
-    const { data } = await FetchJSON<APIResult<APIManga>>(request);
+async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string, adult: boolean): Promise<Manga[]> {
+    const request = new Request(new URL(`${apiUrl}/query?perPage=100&page=${page}&adult=${adult}`));
+    const { data } = await FetchJSON<APIResult<APIMangaV1[]>>(request);
     if (data.length) {
-        return data.map((manga) => new Manga(this, provider, manga.series_slug, manga.title));
+        return data.map((manga) => new Manga(this, provider, JSON.stringify({ id: manga.id.toString(), slug: manga.series_slug }), manga.title));
     }
     return [];
 }
@@ -162,29 +183,68 @@ export function MangasMultiPageAJAX(apiUrl: string, throttle = 0) {
  * @param manga - A reference to the {@link Manga} which shall be assigned as parent for the extracted chapters
  * @param apiUrl - The url of the HeanCMS api for the website
  */
-export async function FetchChaptersSinglePageAJAX(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
-    const request = new Request(new URL(`/series/${manga.Identifier}`, apiUrl));
-    const { seasons } = await FetchJSON<APIManga>(request);
-    const chapterList: Chapter[] = [];
-
-    seasons.map((season) => season.chapters.map((chapter) => {
-        const id = chapter.chapter_slug;
-        const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
-        chapterList.push(new Chapter(this, manga, id, title));
-    }));
-    return chapterList;
+export async function FetchChaptersSinglePageAJAXv1(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
+    try {
+        const mangaslug = (JSON.parse(manga.Identifier) as MangaOrChapterId).slug;
+        const request = new Request(new URL(`${apiUrl}/series/${mangaslug}`));
+        const { seasons } = await FetchJSON<APIMangaV1>(request);
+        const chapterList: Chapter[] = [];
+
+        seasons.map((season) => season.chapters.map((chapter) => {
+            const id = JSON.stringify({
+                id: chapter.id.toString(),
+                slug: chapter.chapter_slug
+            });
+            const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
+            chapterList.push(new Chapter(this, manga, id, title));
+        }));
+        return chapterList;
+    } catch (error) {
+        return [];
+    }
+}
+
+/**
+ * A class decorator that adds the ability to extract all chapters for a given manga from this website using the HeanCMS api url {@link apiUrl}.
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export function ChaptersSinglePageAJAXv1(apiUrl: string) {
+    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
+        Common.ThrowOnUnsupportedDecoratorContext(context);
+        return class extends ctor {
+            public async FetchChapters(this: MangaScraper, manga: Manga): Promise<Chapter[]> {
+                return FetchChaptersSinglePageAJAXv1.call(this, manga, apiUrl);
+            }
+        };
+    };
+}
+
+/**
+ * An extension method for extracting all chapters for the given {@link manga} using the HeanCMS api url {@link apiUrl}.
+ * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
+ * @param manga - A reference to the {@link Manga} which shall be assigned as parent for the extracted chapters
+ * @param apiUrl - The url of the HeanCMS api for the website
+ */
+export async function FetchChaptersSinglePageAJAXv2(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
+    const mangaid: MangaOrChapterId = JSON.parse(manga.Identifier);
+    const { data } = await FetchJSON<APIResult<APIChapter[]>>(new Request(new URL(`${apiUrl}/chapter/query?series_id=${mangaid.id}&perPage=9999&page=1`)));
+    return data.map(chapter => new Chapter(this, manga, JSON.stringify({
+        id: chapter.id.toString(),
+        slug: chapter.chapter_slug,
+    }), `${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim()));
+
 }
 
 /**
  * A class decorator that adds the ability to extract all chapters for a given manga from this website using the HeanCMS api url {@link apiUrl}.
  * @param apiUrl - The url of the HeanCMS api for the website
  */
-export function ChaptersSinglePageAJAX(apiUrl: string) {
+export function ChaptersSinglePageAJAXv2(apiUrl: string) {
     return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
         Common.ThrowOnUnsupportedDecoratorContext(context);
         return class extends ctor {
             public async FetchChapters(this: MangaScraper, manga: Manga): Promise<Chapter[]> {
-                return FetchChaptersSinglePageAJAX.call(this, manga, apiUrl);
+                return FetchChaptersSinglePageAJAXv2.call(this, manga, apiUrl);
             }
         };
     };
@@ -200,9 +260,24 @@ export function ChaptersSinglePageAJAX(apiUrl: string) {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page[]> {
-    const request = new Request(new URL(`/chapter/${chapter.Parent.Identifier}/${chapter.Identifier}`, apiUrl));
-    const { chapter_type, data, paywall, chapter: { storage } } = await FetchJSON<APIPages>(request);
+    const chapterid: MangaOrChapterId = JSON.parse(chapter.Identifier);
+    const mangaid: MangaOrChapterId = JSON.parse(chapter.Parent.Identifier);
+    const request = new Request(new URL(`${apiUrl}/chapter/${mangaid.slug}/${chapterid.slug}`));
+    const data = await FetchJSON<APIPages>(request);
+
+    // check for paywall
+    if (data.paywall) {
+        throw new Error(`${chapter.Title} is paywalled. Please login.`);
+    }
+    // check if novel
+    if (data.chapter.chapter_type.toLowerCase() === 'novel') {
+        return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: data.chapter_type })];
+    }
+
+    const listImages = data.data as string[] || data.chapter.chapter_data.images;
+    return listImages.map(image => new Page(this, chapter, computePageUrl(image, data.chapter.storage, apiUrl), { type: data.chapter.chapter_type }));
 
+    /*
     if (paywall) {
         throw new Error(`${chapter.Title} is paywalled. Please login.`); //localize this
     }
@@ -215,7 +290,7 @@ export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chap
         return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: chapter_type })];
     }
 
-    return (data as string[]).map(image => new Page(this, chapter, computePageUrl(image, storage, apiUrl), { type: chapter_type }));
+    return (data as string[]).map(image => new Page(this, chapter, computePageUrl(image, storage, apiUrl), { type: chapter_type }));*/
 }
 
 /**
@@ -281,6 +356,6 @@ export function ImageAjax(detectMimeType = false, deProxifyLink = true, novelScr
 function computePageUrl(image: string, storage: string, apiUrl: string): URL {
     switch (storage) {
         case "s3": return new URL(image);
-        case "local": return new URL(image, apiUrl);
+        case "local": return new URL(`${apiUrl}/${image}`);
     }
 }

From 09180560ddc9492a3c018e0bde99aff8a8593b9a Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 14 Apr 2024 11:18:51 +0200
Subject: [PATCH 15/31] better code

---
 web/src/engine/websites/decorators/HeanCMS.ts | 20 ++++++++-----------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index 650e46ba81..5e1bb9cde0 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -134,19 +134,15 @@ export function MangaCSS(pattern: RegExp, apiURL: string) {
  */
 export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: MangaPlugin, apiUrl: string, throttle = 0): Promise<Manga[]> {
     const mangaList: Manga[] = [];
-    //First loop get adult mangas
-    for (let page = 1, run = true; run; page++) {
-        const mangas = await getMangaFromPage.call(this, provider, page, apiUrl, true);
-        mangas.length > 0 ? mangaList.push(...mangas) : run = false;
-        await new Promise(resolve => setTimeout(resolve, throttle));
-    }
-    //First loop get non adult mangas
-    for (let page = 1, run = true; run; page++) {
-        const mangas = await getMangaFromPage.call(this, provider, page, apiUrl, false);
-        mangas.length > 0 ? mangaList.push(...mangas) : run = false;
-        await new Promise(resolve => setTimeout(resolve, throttle));
+
+    for (const adult of [true, false]) { //there is no "dont care if adult or not flag"" on "new" api, and old dont care about the flag
+        for (let page = 1, run = true; run; page++) {
+            const mangas = await getMangaFromPage.call(this, provider, page, apiUrl, adult);
+            mangas.length > 0 ? mangaList.push(...mangas) : run = false;
+            await new Promise(resolve => setTimeout(resolve, throttle));
+        }
     }
-    return mangaList.distinct();
+    return mangaList.distinct();//filter in case of old api
 }
 async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string, adult: boolean): Promise<Manga[]> {
     const request = new Request(new URL(`${apiUrl}/query?perPage=100&page=${page}&adult=${adult}`));

From 4008c38ca4501db0bdd94ef2e4f87f439941f9e0 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 14 Apr 2024 11:20:14 +0200
Subject: [PATCH 16/31] Modescanlor : use HeanCMS

---
 web/src/engine/websites/ModeScanlator.ts     | 79 +++-----------------
 web/src/engine/websites/ModeScanlator_e2e.ts | 14 ++--
 2 files changed, 16 insertions(+), 77 deletions(-)

diff --git a/web/src/engine/websites/ModeScanlator.ts b/web/src/engine/websites/ModeScanlator.ts
index 4b1f2018a3..2e992da5da 100644
--- a/web/src/engine/websites/ModeScanlator.ts
+++ b/web/src/engine/websites/ModeScanlator.ts
@@ -1,83 +1,22 @@
 import { Tags } from '../Tags';
 import icon from './ModeScanlator.webp';
-import { type Chapter, DecoratableMangaScraper, Page } from '../providers/MangaPlugin';
-import * as Common from './decorators/Common';
-import { Fetch, FetchWindowScript } from '../platform/FetchProvider';
-import * as JSZip from 'jszip';
-import type { Priority } from '../taskpool/TaskPool';
+import { DecoratableMangaScraper } from '../providers/MangaPlugin';
+import * as HeamCMS from './decorators/HeanCMS';
 
-const pagescript = `
-    new Promise((resolve, reject) => {
-        try {
-            resolve(urls);
-        } catch (error) {
-            const pages = [...document.querySelectorAll('div#imageContainer img')].map(image=> new URL(image.src, window.location.origin).href);
-            resolve(pages);
-        }
-    });
-`;
+const apiUrl = 'https://api.modescanlator.com';
 
-function ChapterExtractor(anchor: HTMLAnchorElement) {
-    const id = anchor.pathname;
-    const title = anchor.querySelector('div.info__capitulo__obras span.numero__capitulo').textContent.trim();
-    return { id, title };
-}
+@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
+@HeamCMS.MangasMultiPageAJAX(apiUrl)
+@HeamCMS.ChaptersSinglePageAJAXv2(apiUrl)
+@HeamCMS.PagesSinglePageAJAX(apiUrl)
+@HeamCMS.ImageAjax()
 
-@Common.MangaCSS(/^{origin}\/[^/]+\/$/, 'h1.desc__titulo__comic')
-@Common.MangasSinglePageCSS('/todas-as-obras/', 'div.comics__all__box a.titulo__comic__allcomics')
-@Common.ChaptersSinglePageCSS('ul.capitulos__lista a.link__capitulos', ChapterExtractor)
 export default class extends DecoratableMangaScraper {
 
     public constructor() {
-        super('modescanlator', `Mode Scanlator`, 'https://modescanlator.com', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator), Tags.Accessibility.RegionLocked;
+        super('modescanlator', `Mode Scanlator`, 'https://modescanlator.com', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator, Tags.Accessibility.RegionLocked);
     }
     public override get Icon() {
         return icon;
     }
-
-    public override async FetchPages(chapter: Chapter): Promise<Page[]> {
-        //1st : first fetch zip files urls
-        let request = new Request(new URL(chapter.Identifier, this.URI).href);
-        const files: string[] = await FetchWindowScript(request, pagescript);
-        if (files.length == 0) return [];
-
-        //if files are zip
-        if (files[0].endsWith('.zip')) {
-            //handle zip files
-            const pages : Page[]= [];
-            for (const zipurl of files) {
-                request = new Request(new URL(zipurl, this.URI).href);
-                const response = await Fetch(request);
-                const zipdata = await response.arrayBuffer();
-                const zipfile = await JSZip.loadAsync(zipdata);
-                const fileNames = Object.keys(zipfile.files).sort((a, b) => this.extractNumber(a) - this.extractNumber(b));
-                for (const fileName of fileNames) {
-                    if (!fileName.match(/\.(s)$/i)) { //if extension is not .s (for svg), its a picture
-                        pages.push(new Page(this, chapter, new URL(zipurl, this.URI), { filename: fileName }));
-                    }
-                }
-            }
-            return pages;
-
-        } else { //we have normal pictures links
-            return files.map(file => new Page(this, chapter, new URL(file)));
-        }
-
-    }
-    extractNumber(fileName) : number {
-        return parseInt(fileName.split(".")[0]);
-    }
-
-    public override async FetchImage(page: Page, priority: Priority, signal: AbortSignal): Promise<Blob> {
-        if (page.Link.href.endsWith('.zip')) {
-            const request = new Request(new URL(page.Link, this.URI).href);
-            const response = await Fetch(request);
-            const zipdata = await response.arrayBuffer();
-            const zipfile = await JSZip.loadAsync(zipdata);
-            const zipEntry = zipfile.files[page.Parameters['filename'] as string];
-            const imagebuffer = await zipEntry.async('nodebuffer');
-            return Common.GetTypedData(imagebuffer);
-        } else return Common.FetchImageAjax.call(this, page, priority, signal);
-    }
-
 }
\ No newline at end of file
diff --git a/web/src/engine/websites/ModeScanlator_e2e.ts b/web/src/engine/websites/ModeScanlator_e2e.ts
index 924bbb256d..f8aac6ea05 100644
--- a/web/src/engine/websites/ModeScanlator_e2e.ts
+++ b/web/src/engine/websites/ModeScanlator_e2e.ts
@@ -6,18 +6,18 @@ const config: Config = {
         title: 'Mode Scanlator'
     },
     container: {
-        url: 'https://modescanlator.com/eternal-first-son-in-law/',
-        id: '/eternal-first-son-in-law/',
-        title: 'Eternal First Son-In-Law',
+        url: 'https://modescanlator.com/series/uma-lenda-do-vento',
+        id: JSON.stringify({ id: '36', slug: 'uma-lenda-do-vento' }),
+        title: 'Uma Lenda do Vento',
     },
     child: {
-        id: '/eternal-first-son-in-law/299/',
-        title: 'Capítulo 299',
+        id: JSON.stringify({ id: '2420', slug: 'capitulo-126' }),
+        title: 'Capítulo 126',
     },
     entry: {
         index: 0,
-        size: 157_765,
-        type: 'image/avif'
+        size: 1_222_675,
+        type: 'image/jpeg'
     }
 };
 

From e4c5aff4fb75651e12932b83b8248624b292b688 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sat, 27 Apr 2024 14:04:24 +0200
Subject: [PATCH 17/31] fix case

---
 web/src/engine/websites/decorators/HeanCMS.ts | 23 ++++---------------
 1 file changed, 4 insertions(+), 19 deletions(-)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index 5e1bb9cde0..b57dc1b894 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -137,14 +137,14 @@ export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: Man
 
     for (const adult of [true, false]) { //there is no "dont care if adult or not flag"" on "new" api, and old dont care about the flag
         for (let page = 1, run = true; run; page++) {
-            const mangas = await getMangaFromPage.call(this, provider, page, apiUrl, adult);
+            const mangas = await GetMangaFromPage.call(this, provider, page, apiUrl, adult);
             mangas.length > 0 ? mangaList.push(...mangas) : run = false;
             await new Promise(resolve => setTimeout(resolve, throttle));
         }
     }
     return mangaList.distinct();//filter in case of old api
 }
-async function getMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string, adult: boolean): Promise<Manga[]> {
+async function GetMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string, adult: boolean): Promise<Manga[]> {
     const request = new Request(new URL(`${apiUrl}/query?perPage=100&page=${page}&adult=${adult}`));
     const { data } = await FetchJSON<APIResult<APIMangaV1[]>>(request);
     if (data.length) {
@@ -271,22 +271,7 @@ export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chap
     }
 
     const listImages = data.data as string[] || data.chapter.chapter_data.images;
-    return listImages.map(image => new Page(this, chapter, computePageUrl(image, data.chapter.storage, apiUrl), { type: data.chapter.chapter_type }));
-
-    /*
-    if (paywall) {
-        throw new Error(`${chapter.Title} is paywalled. Please login.`); //localize this
-    }
-
-    //in case of novel data is the html string, in case of comic its an array of strings (pictures urls or pathnames)
-    if (!data || data.length < 1) return [];
-
-    // check if novel
-    if (chapter_type.toLowerCase() === 'novel') {
-        return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: chapter_type })];
-    }
-
-    return (data as string[]).map(image => new Page(this, chapter, computePageUrl(image, storage, apiUrl), { type: chapter_type }));*/
+    return listImages.map(image => new Page(this, chapter, ComputePageUrl(image, data.chapter.storage, apiUrl), { type: data.chapter.chapter_type }));
 }
 
 /**
@@ -349,7 +334,7 @@ export function ImageAjax(detectMimeType = false, deProxifyLink = true, novelScr
  * @param storage - As string representing the type of storage used : "s3" or "local"
  * @param apiUrl - The url of the HeanCMS api for the website *
  */
-function computePageUrl(image: string, storage: string, apiUrl: string): URL {
+function ComputePageUrl(image: string, storage: string, apiUrl: string): URL {
     switch (storage) {
         case "s3": return new URL(image);
         case "local": return new URL(`${apiUrl}/${image}`);

From 9ec054e36f83d0ee3a781374cf76ec40f6097ae7 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Tue, 7 May 2024 12:10:49 +0200
Subject: [PATCH 18/31] update e2e tests to use vitest

---
 web/src/engine/websites/OmegaScans_e2e.ts | 7 ++++---
 web/src/engine/websites/PerfScan_e2e.ts   | 7 ++++---
 web/src/engine/websites/TempleScan_e2e.ts | 5 +++--
 3 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/web/src/engine/websites/OmegaScans_e2e.ts b/web/src/engine/websites/OmegaScans_e2e.ts
index b404947985..f68697f47e 100644
--- a/web/src/engine/websites/OmegaScans_e2e.ts
+++ b/web/src/engine/websites/OmegaScans_e2e.ts
@@ -1,4 +1,5 @@
-import { TestFixture } from '../../../test/WebsitesFixture';
+import { describe } from 'vitest';
+import { TestFixture } from '../../../test/WebsitesFixture';
 
 const ComicConfig = {
     plugin: {
@@ -22,7 +23,7 @@ const ComicConfig = {
 };
 
 const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
+describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
 
 const NovelConfig = {
     plugin: {
@@ -46,4 +47,4 @@ const NovelConfig = {
 };
 
 const NovelFixture = new TestFixture(NovelConfig);
-describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
\ No newline at end of file
+describe(NovelFixture.Name, async () => (await NovelFixture.Connect()).AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/PerfScan_e2e.ts b/web/src/engine/websites/PerfScan_e2e.ts
index c3c945daaa..f1e4d4344b 100644
--- a/web/src/engine/websites/PerfScan_e2e.ts
+++ b/web/src/engine/websites/PerfScan_e2e.ts
@@ -1,4 +1,5 @@
-import { TestFixture } from '../../../test/WebsitesFixture';
+import { describe } from 'vitest';
+import { TestFixture } from '../../../test/WebsitesFixture';
 
 const ComicConfig = {
     plugin: {
@@ -22,7 +23,7 @@ const ComicConfig = {
 };
 
 const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
+describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
 
 const NovelConfig = {
     plugin: {
@@ -46,4 +47,4 @@ const NovelConfig = {
 };
 
 const NovelFixture = new TestFixture(NovelConfig);
-describe(NovelFixture.Name, () => NovelFixture.AssertWebsite());
+describe(NovelFixture.Name, async () => (await NovelFixture.Connect()).AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/TempleScan_e2e.ts b/web/src/engine/websites/TempleScan_e2e.ts
index f13bd8e16c..f883e0af3a 100644
--- a/web/src/engine/websites/TempleScan_e2e.ts
+++ b/web/src/engine/websites/TempleScan_e2e.ts
@@ -1,4 +1,5 @@
-import { TestFixture } from '../../../test/WebsitesFixture';
+import { describe } from 'vitest';
+import { TestFixture } from '../../../test/WebsitesFixture';
 
 const ComicConfig = {
     plugin: {
@@ -22,4 +23,4 @@ const ComicConfig = {
 };
 
 const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, () => ComicFixture.AssertWebsite());
\ No newline at end of file
+describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
\ No newline at end of file

From e2effeb58ee80152df9b5f50744bd79a66a23b7a Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Wed, 14 Aug 2024 15:05:16 +0200
Subject: [PATCH 19/31] Update ModeScanlator.ts

---
 web/src/engine/websites/ModeScanlator.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web/src/engine/websites/ModeScanlator.ts b/web/src/engine/websites/ModeScanlator.ts
index 2e992da5da..41f191f888 100644
--- a/web/src/engine/websites/ModeScanlator.ts
+++ b/web/src/engine/websites/ModeScanlator.ts
@@ -14,7 +14,7 @@ const apiUrl = 'https://api.modescanlator.com';
 export default class extends DecoratableMangaScraper {
 
     public constructor() {
-        super('modescanlator', `Mode Scanlator`, 'https://modescanlator.com', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator, Tags.Accessibility.RegionLocked);
+        super('modescanlator', `Mode Scanlator`, 'https://site.modescanlator.com', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator);
     }
     public override get Icon() {
         return icon;

From 6b5f32addb83d2dd6b3ca47ff6f0577ec89cb291 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Wed, 14 Aug 2024 15:17:27 +0200
Subject: [PATCH 20/31] Update HeanCMS.ts

---
 web/src/engine/websites/decorators/HeanCMS.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index b57dc1b894..b91127140a 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -195,6 +195,7 @@ export async function FetchChaptersSinglePageAJAXv1(this: MangaScraper, manga: M
             chapterList.push(new Chapter(this, manga, id, title));
         }));
         return chapterList;
+        /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
     } catch (error) {
         return [];
     }

From 33840ac73e170fc253d70491d289f7be300645c0 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Wed, 14 Aug 2024 21:59:07 +0200
Subject: [PATCH 21/31] fix modescanlator tests

---
 web/src/engine/websites/ModeScanlator_e2e.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web/src/engine/websites/ModeScanlator_e2e.ts b/web/src/engine/websites/ModeScanlator_e2e.ts
index a1560455eb..88da43d0e5 100644
--- a/web/src/engine/websites/ModeScanlator_e2e.ts
+++ b/web/src/engine/websites/ModeScanlator_e2e.ts
@@ -7,7 +7,7 @@ const config: Config = {
         title: 'Mode Scanlator'
     },
     container: {
-        url: 'https://modescanlator.com/series/uma-lenda-do-vento',
+        url: 'https://site.modescanlator.com/series/uma-lenda-do-vento',
         id: JSON.stringify({ id: '36', slug: 'uma-lenda-do-vento' }),
         title: 'Uma Lenda do Vento',
     },

From 06795369ae2bd14488b698aec015902d21a96297 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Wed, 21 Aug 2024 17:30:23 +0200
Subject: [PATCH 22/31] remove TempleScan

no more HeanCMS
---
 web/src/engine/websites/TempleScan.ts     |  22 ------------------
 web/src/engine/websites/TempleScan.webp   | Bin 1950 -> 0 bytes
 web/src/engine/websites/TempleScan_e2e.ts |  26 ----------------------
 web/src/engine/websites/_index.ts         |   2 ++
 4 files changed, 2 insertions(+), 48 deletions(-)
 delete mode 100644 web/src/engine/websites/TempleScan.ts
 delete mode 100644 web/src/engine/websites/TempleScan.webp
 delete mode 100644 web/src/engine/websites/TempleScan_e2e.ts

diff --git a/web/src/engine/websites/TempleScan.ts b/web/src/engine/websites/TempleScan.ts
deleted file mode 100644
index 7efdd73540..0000000000
--- a/web/src/engine/websites/TempleScan.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Tags } from '../Tags';
-import icon from './TempleScan.webp';
-import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as HeamCMS from './decorators/HeanCMS';
-
-const apiUrl = 'https://templescan.net/apiv1';
-
-@HeamCMS.MangaCSS(/^{origin}\/comic\/[^/]+$/, apiUrl)
-@HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAXv1(apiUrl)
-@HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax(true)
-export default class extends DecoratableMangaScraper {
-
-    public constructor() {
-        super('templescan', 'TempleScan', 'https://templescan.net', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Language.English, Tags.Source.Scanlator);
-    }
-
-    public override get Icon() {
-        return icon;
-    }
-}
\ No newline at end of file
diff --git a/web/src/engine/websites/TempleScan.webp b/web/src/engine/websites/TempleScan.webp
deleted file mode 100644
index a85135682e061a2f4dfc9605cb86a1bd38cfa1cf..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1950
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

diff --git a/web/src/engine/websites/TempleScan_e2e.ts b/web/src/engine/websites/TempleScan_e2e.ts
deleted file mode 100644
index f883e0af3a..0000000000
--- a/web/src/engine/websites/TempleScan_e2e.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { describe } from 'vitest';
-import { TestFixture } from '../../../test/WebsitesFixture';
-
-const ComicConfig = {
-    plugin: {
-        id: 'templescan',
-        title: 'TempleScan'
-    },
-    container: {
-        url: 'https://templescan.net/comic/predatory-marriage-complete-edition',
-        id: JSON.stringify({ id: '55', slug: 'predatory-marriage-complete-edition' }),
-        title: `Predatory Marriage (Complete Edition)`
-    },
-    child: {
-        id: JSON.stringify({ id: '1028', slug: '83662-chapter-15' }),
-        title: 'Chapter 15'
-    },
-    entry: {
-        index: 0,
-        size: 2_953_394,
-        type: 'image/jpeg'
-    }
-};
-
-const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index 03fdf40e7b..540902ec3d 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -501,6 +501,7 @@ export { default as NovelMic } from './NovelMic';
 export { default as NvManga } from './NvManga';
 export { default as Nyrax } from './Nyrax';
 export { default as OlympusScanlation } from './OlympusScanlation';
+export { default as OmegaScans } from './OmegaScans';
 export { default as OnMangaMe } from './OnMangaMe';
 export { default as Opiatoon } from './Opiatoon';
 export { default as Oremanga } from './Oremanga';
@@ -513,6 +514,7 @@ export { default as PCNet } from './PCNet';
 export { default as PeanuToon } from './PeanuToon';
 export { default as PelaTeam } from './PelaTeam';
 export { default as Penlab } from './Penlab';
+export { default as PerfScan } from './PerfScan';
 export { default as PhenixScans } from './PhenixScans';
 export { default as PhoenixScansIT } from './PhoenixScansIT';
 export { default as Piccoma } from './Piccoma';

From 32b81f024f05903a3934728e632a7d3b3c2252fd Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 23 Sep 2024 13:22:14 +0200
Subject: [PATCH 23/31] Update HeanCMS.ts

---
 web/src/engine/websites/decorators/HeanCMS.ts | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index b91127140a..a4ebb72f47 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -147,10 +147,8 @@ export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: Man
 async function GetMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string, adult: boolean): Promise<Manga[]> {
     const request = new Request(new URL(`${apiUrl}/query?perPage=100&page=${page}&adult=${adult}`));
     const { data } = await FetchJSON<APIResult<APIMangaV1[]>>(request);
-    if (data.length) {
-        return data.map((manga) => new Manga(this, provider, JSON.stringify({ id: manga.id.toString(), slug: manga.series_slug }), manga.title));
-    }
-    return [];
+    return !data.length ? [] : data.map((manga) => new Manga(this, provider, JSON.stringify({ id: manga.id.toString(), slug: manga.series_slug }), manga.title));
+
 }
 
 /**
@@ -195,8 +193,7 @@ export async function FetchChaptersSinglePageAJAXv1(this: MangaScraper, manga: M
             chapterList.push(new Chapter(this, manga, id, title));
         }));
         return chapterList;
-        /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
-    } catch (error) {
+    } catch {
         return [];
     }
 }
@@ -262,11 +259,10 @@ export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chap
     const request = new Request(new URL(`${apiUrl}/chapter/${mangaid.slug}/${chapterid.slug}`));
     const data = await FetchJSON<APIPages>(request);
 
-    // check for paywall
     if (data.paywall) {
         throw new Error(`${chapter.Title} is paywalled. Please login.`);
     }
-    // check if novel
+
     if (data.chapter.chapter_type.toLowerCase() === 'novel') {
         return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: data.chapter_type })];
     }

From 5d1a5a9ffabff31a9f03ed7f8e55015b5a1c9ede Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sat, 28 Sep 2024 13:04:19 +0200
Subject: [PATCH 24/31] remove novel support & multiple fixes

---
 web/src/engine/websites/ModeScanlator.ts      |  4 +-
 web/src/engine/websites/ModeScanlator_e2e.ts  |  3 +-
 web/src/engine/websites/OmegaScans_e2e.ts     | 26 +----
 web/src/engine/websites/decorators/HeanCMS.ts | 97 +++++++------------
 web/src/i18n/ILocale.ts                       |  5 +
 web/src/i18n/locales/en_US.ts                 |  2 +
 6 files changed, 46 insertions(+), 91 deletions(-)

diff --git a/web/src/engine/websites/ModeScanlator.ts b/web/src/engine/websites/ModeScanlator.ts
index 41f191f888..80bfb94b4b 100644
--- a/web/src/engine/websites/ModeScanlator.ts
+++ b/web/src/engine/websites/ModeScanlator.ts
@@ -3,7 +3,7 @@ import icon from './ModeScanlator.webp';
 import { DecoratableMangaScraper } from '../providers/MangaPlugin';
 import * as HeamCMS from './decorators/HeanCMS';
 
-const apiUrl = 'https://api.modescanlator.com';
+const apiUrl = 'https://api.modescanlator.net';
 
 @HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
 @HeamCMS.MangasMultiPageAJAX(apiUrl)
@@ -14,7 +14,7 @@ const apiUrl = 'https://api.modescanlator.com';
 export default class extends DecoratableMangaScraper {
 
     public constructor() {
-        super('modescanlator', `Mode Scanlator`, 'https://site.modescanlator.com', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator);
+        super('modescanlator', `Mode Scanlator`, 'https://site.modescanlator.net', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator);
     }
     public override get Icon() {
         return icon;
diff --git a/web/src/engine/websites/ModeScanlator_e2e.ts b/web/src/engine/websites/ModeScanlator_e2e.ts
index 88da43d0e5..e574def997 100644
--- a/web/src/engine/websites/ModeScanlator_e2e.ts
+++ b/web/src/engine/websites/ModeScanlator_e2e.ts
@@ -7,9 +7,10 @@ const config: Config = {
         title: 'Mode Scanlator'
     },
     container: {
-        url: 'https://site.modescanlator.com/series/uma-lenda-do-vento',
+        url: 'https://site.modescanlator.net/series/uma-lenda-do-vento',
         id: JSON.stringify({ id: '36', slug: 'uma-lenda-do-vento' }),
         title: 'Uma Lenda do Vento',
+        timeout: 10000
     },
     child: {
         id: JSON.stringify({ id: '2420', slug: 'capitulo-126' }),
diff --git a/web/src/engine/websites/OmegaScans_e2e.ts b/web/src/engine/websites/OmegaScans_e2e.ts
index f68697f47e..163a757026 100644
--- a/web/src/engine/websites/OmegaScans_e2e.ts
+++ b/web/src/engine/websites/OmegaScans_e2e.ts
@@ -23,28 +23,4 @@ const ComicConfig = {
 };
 
 const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
-
-const NovelConfig = {
-    plugin: {
-        id: 'omegascans',
-        title: 'OmegaScans'
-    },
-    container: {
-        url: 'https://omegascans.org/series/the-scion-of-the-labyrinth-city',
-        id: JSON.stringify({ id: '186', slug: 'the-scion-of-the-labyrinth-city' }),
-        title: 'The Scion of the Labyrinth City'
-    },
-    child: {
-        id: JSON.stringify({ id: '3226', slug: 'chapter-28' }),
-        title: 'Chapter 28'
-    },
-    entry: {
-        index: 0,
-        size: 892_312,
-        type: 'image/png'
-    }
-};
-
-const NovelFixture = new TestFixture(NovelConfig);
-describe(NovelFixture.Name, async () => (await NovelFixture.Connect()).AssertWebsite());
\ No newline at end of file
+describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
\ No newline at end of file
diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
index a4ebb72f47..112cf0df2e 100644
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ b/web/src/engine/websites/decorators/HeanCMS.ts
@@ -1,40 +1,11 @@
-import { FetchJSON, FetchWindowScript } from '../../platform/FetchProvider';
+import { Exception } from '../../Error';
+import { FetchJSON } from '../../platform/FetchProvider';
 import { type MangaScraper, type MangaPlugin, Manga, Chapter, Page } from '../../providers/MangaPlugin';
 import type { Priority } from '../../taskpool/TaskPool';
 import * as Common from './Common';
+import { WebsiteResourceKey as R } from '../../../i18n/ILocale';
 
-//TODO: get novel color theme from settings and apply them to the script somehow. Setting must ofc have been initializated before
-//add a listener on the aforementionned setting
-
-const DefaultNovelScript = `
-    new Promise((resolve, reject) => {
-        document.body.style.width = '56em';
-        let container = document.querySelector('div.container');
-        container.style.maxWidth = '56em';
-        container.style.padding = '0';
-        container.style.margin = '0';
-        let novel = document.querySelector('div#reader-container');
-        novel.style.padding = '1.5em';
-        [...novel.querySelectorAll(":not(:empty)")].forEach(ele => {
-            ele.style.backgroundColor = 'black'
-            ele.style.color = 'white'
-        })
-        novel.style.backgroundColor = 'black'
-        novel.style.color = 'white'
-        let script = document.createElement('script');
-        script.onerror = error => reject(error);
-        script.onload = async function() {
-            try {
-                let canvas = await html2canvas(novel);
-                resolve([canvas.toDataURL('image/png')]);
-            } catch (error) {
-                reject(error)
-            }
-        }
-        script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
-        document.body.appendChild(script);
-    });
- `;
+// TODO: Add Novel support
 
 type APIMangaV1 = {
     title: string
@@ -80,6 +51,10 @@ type MangaOrChapterId = {
     slug: string
 }
 
+type PageType = {
+    type: 'Comic' | 'Novel'
+}
+
 /***************************************************
  ******** Manga from URL Extraction Methods ********
  ***************************************************/
@@ -92,7 +67,7 @@ type MangaOrChapterId = {
  * @param apiUrl - The url of the HeanCMS api for the website
  */
 export async function FetchMangaCSS(this: MangaScraper, provider: MangaPlugin, url: string, apiUrl: string): Promise<Manga> {
-    const slug = new URL(url).pathname.split('/')[2];
+    const slug = new URL(url).pathname.split('/').at(-1);
     const { title, series_slug, id } = await FetchJSON<APIMangaV1>(new Request(new URL(`${apiUrl}/series/${slug}`)));
     return new Manga(this, provider, JSON.stringify({
         id: id.toString(),
@@ -135,7 +110,7 @@ export function MangaCSS(pattern: RegExp, apiURL: string) {
 export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: MangaPlugin, apiUrl: string, throttle = 0): Promise<Manga[]> {
     const mangaList: Manga[] = [];
 
-    for (const adult of [true, false]) { //there is no "dont care if adult or not flag"" on "new" api, and old dont care about the flag
+    for (const adult of [true, false]) { //there is no "dont care if adult or not flag" on "new" api, and old dont care about the flag
         for (let page = 1, run = true; run; page++) {
             const mangas = await GetMangaFromPage.call(this, provider, page, apiUrl, adult);
             mangas.length > 0 ? mangaList.push(...mangas) : run = false;
@@ -182,17 +157,19 @@ export async function FetchChaptersSinglePageAJAXv1(this: MangaScraper, manga: M
         const mangaslug = (JSON.parse(manga.Identifier) as MangaOrChapterId).slug;
         const request = new Request(new URL(`${apiUrl}/series/${mangaslug}`));
         const { seasons } = await FetchJSON<APIMangaV1>(request);
-        const chapterList: Chapter[] = [];
-
-        seasons.map((season) => season.chapters.map((chapter) => {
-            const id = JSON.stringify({
-                id: chapter.id.toString(),
-                slug: chapter.chapter_slug
+        return seasons.reduce(async (accumulator: Promise<Chapter[]>, season) => {
+            const chapters = season.chapters.map(chapter => {
+                const id = JSON.stringify({
+                    id: chapter.id.toString(),
+                    slug: chapter.chapter_slug
+                });
+                const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
+                return new Chapter(this, manga, id, title);
             });
-            const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
-            chapterList.push(new Chapter(this, manga, id, title));
-        }));
-        return chapterList;
+            (await accumulator).concat(...chapters);
+            return accumulator;
+        }, Promise.resolve<Chapter[]>([]));
+
     } catch {
         return [];
     }
@@ -253,22 +230,21 @@ export function ChaptersSinglePageAJAXv2(apiUrl: string) {
  * @param chapter - A reference to the {@link Chapter} which shall be assigned as parent for the extracted pages
  * @param apiUrl - The url of the HeanCMS api for the website
  */
-export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page[]> {
+export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page<PageType>[]> {
     const chapterid: MangaOrChapterId = JSON.parse(chapter.Identifier);
     const mangaid: MangaOrChapterId = JSON.parse(chapter.Parent.Identifier);
-    const request = new Request(new URL(`${apiUrl}/chapter/${mangaid.slug}/${chapterid.slug}`));
-    const data = await FetchJSON<APIPages>(request);
+    const data = await FetchJSON<APIPages>(new Request(new URL(`${apiUrl}/chapter/${mangaid.slug}/${chapterid.slug}`)));
 
     if (data.paywall) {
-        throw new Error(`${chapter.Title} is paywalled. Please login.`);
+        throw new Exception(R.Plugin_Common_Chapter_UnavailableError);
     }
 
     if (data.chapter.chapter_type.toLowerCase() === 'novel') {
-        return [new Page(this, chapter, new URL(`/series/${chapter.Parent.Identifier}/${chapter.Identifier}`, this.URI), { type: data.chapter_type })];
+        throw new Exception(R.Plugin_HeanCMS_ErrorNovelsNotSupported);
     }
 
-    const listImages = data.data as string[] || data.chapter.chapter_data.images;
-    return listImages.map(image => new Page(this, chapter, ComputePageUrl(image, data.chapter.storage, apiUrl), { type: data.chapter.chapter_type }));
+    const listImages = data.data && Array.isArray(data.data) ? data.data as string[] : data.chapter.chapter_data.images;
+    return listImages.map(image => new Page<PageType>(this, chapter, ComputePageUrl(image, data.chapter.storage, apiUrl), { type: data.chapter.chapter_type }));
 }
 
 /**
@@ -279,7 +255,7 @@ export function PagesSinglePageAJAX(apiUrl: string) {
     return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
         Common.ThrowOnUnsupportedDecoratorContext(context);
         return class extends ctor {
-            public async FetchPages(this: MangaScraper, chapter: Chapter): Promise<Page[]> {
+            public async FetchPages(this: MangaScraper, chapter: Chapter): Promise<Page<PageType>[]> {
                 return FetchPagesSinglePageAJAX.call(this, chapter, apiUrl);
             }
         };
@@ -298,16 +274,11 @@ export function PagesSinglePageAJAX(apiUrl: string) {
  * @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)
  * @param deProxifyLink - Remove common image proxies (default false)
- * @param novelScript  - a custom script to get and transform the novel text into a dataURL
  */
-export async function FetchImageAjax(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType = false, deProxifyLink = true, novelScript = DefaultNovelScript): Promise<Blob> {
-    if (page.Parameters?.type as string === 'Comic') {
+export async function FetchImageAjax(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType = false, deProxifyLink = true): Promise<Blob> {
+    if (page.Parameters?.type === 'Comic') {
         return Common.FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
-    } else {
-        //TODO: test if user want to export the NOVEL as HTML?
-        const data = await FetchWindowScript<string>(new Request(page.Link), novelScript, 1000, 10000);
-        return Common.FetchImageAjax.call(this, new Page(this, page.Parent as Chapter, new URL(data)), priority, signal, false, false);
-    }
+    } else throw new Exception(R.Plugin_HeanCMS_ErrorNovelsNotSupported);
 }
 
 /**
@@ -315,12 +286,12 @@ export async function FetchImageAjax(this: MangaScraper, page: Page, priority: P
  * @param detectMimeType - Force a fingerprint check of the image data to detect its mime-type (instead of relying on the Content-Type header)
  * @param deProxifyLink - Remove common image proxies (default false)
  */
-export function ImageAjax(detectMimeType = false, deProxifyLink = true, novelScript: string = DefaultNovelScript) {
+export function ImageAjax(detectMimeType = false, deProxifyLink = true) {
     return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
         Common.ThrowOnUnsupportedDecoratorContext(context);
         return class extends ctor {
             public async FetchImage(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal): Promise<Blob> {
-                return FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink, novelScript);
+                return FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
             }
         };
     };
diff --git a/web/src/i18n/ILocale.ts b/web/src/i18n/ILocale.ts
index 1259a45786..1734c34549 100644
--- a/web/src/i18n/ILocale.ts
+++ b/web/src/i18n/ILocale.ts
@@ -358,6 +358,11 @@ export enum WebsiteResourceKey {
     Plugin_SheepScanlations_Settings_PasswordInfo = 'Plugin_SheepScanlations_Settings_PasswordInfo',
 }
 
+// [SECTION]: Template : HEANCMS
+export enum WebsiteResourceKey {
+    Plugin_HeanCMS_ErrorNovelsNotSupported = 'Plugin_HeanCMS_ErrorNovelsNotSupported'
+}
+
 export const VariantResourceKey = {
     ...TagCategoryResourceKey,
     ...TagResourceKey,
diff --git a/web/src/i18n/locales/en_US.ts b/web/src/i18n/locales/en_US.ts
index 30ab0e01e8..ed15c75d7a 100644
--- a/web/src/i18n/locales/en_US.ts
+++ b/web/src/i18n/locales/en_US.ts
@@ -315,6 +315,8 @@ const translations: VariantResource = {
 
   Plugin_CuuTruyen_Error_NotProcessed: 'This chapter is still processing, please try again later.',
 
+  Plugin_HeanCMS_ErrorNovelsNotSupported: 'Novels are not (yet) supported in Hakuneko !',
+
   Plugin_PocketComics_LanguageMismatchError: 'Unable to find manga {0} for selected language {1}',
 
   Plugin_SheepScanlations_Settings_Username: 'Username',

From 1f6e066e4a15a76eeaf1d117d65a48d1cb2a6a31 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 13 Oct 2024 11:13:05 +0200
Subject: [PATCH 25/31] turn HeanCMS into a template

---
 web/src/engine/websites/ModeScanlator.ts      |  15 +-
 web/src/engine/websites/OmegaScans.ts         |  12 +-
 web/src/engine/websites/PerfScan.ts           |  12 +-
 web/src/engine/websites/decorators/HeanCMS.ts | 310 ------------------
 web/src/engine/websites/templates/HeanCMS.ts  | 125 +++++++
 5 files changed, 133 insertions(+), 341 deletions(-)
 delete mode 100644 web/src/engine/websites/decorators/HeanCMS.ts
 create mode 100644 web/src/engine/websites/templates/HeanCMS.ts

diff --git a/web/src/engine/websites/ModeScanlator.ts b/web/src/engine/websites/ModeScanlator.ts
index 80bfb94b4b..d53b5177f2 100644
--- a/web/src/engine/websites/ModeScanlator.ts
+++ b/web/src/engine/websites/ModeScanlator.ts
@@ -1,21 +1,14 @@
 import { Tags } from '../Tags';
 import icon from './ModeScanlator.webp';
-import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as HeamCMS from './decorators/HeanCMS';
+import { HeanCMS } from './templates/HeanCMS';
 
-const apiUrl = 'https://api.modescanlator.net';
-
-@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
-@HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAXv2(apiUrl)
-@HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax()
-
-export default class extends DecoratableMangaScraper {
+export default class extends HeanCMS {
 
     public constructor() {
         super('modescanlator', `Mode Scanlator`, 'https://site.modescanlator.net', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator);
+        this.apiUrl = this.URI.origin.replace('site', 'api');
     }
+
     public override get Icon() {
         return icon;
     }
diff --git a/web/src/engine/websites/OmegaScans.ts b/web/src/engine/websites/OmegaScans.ts
index 6bd81f03c7..8049fb5153 100644
--- a/web/src/engine/websites/OmegaScans.ts
+++ b/web/src/engine/websites/OmegaScans.ts
@@ -1,16 +1,8 @@
 import { Tags } from '../Tags';
 import icon from './OmegaScans.webp';
-import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as HeamCMS from './decorators/HeanCMS';
+import { HeanCMS } from './templates/HeanCMS';
 
-const apiUrl = 'https://api.omegascans.org';
-
-@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
-@HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAXv2(apiUrl)
-@HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax(true)
-export default class extends DecoratableMangaScraper {
+export default class extends HeanCMS {
 
     public constructor() {
         super('omegascans', 'OmegaScans', 'https://omegascans.org', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Novel, Tags.Language.English, Tags.Source.Scanlator, Tags.Rating.Pornographic);
diff --git a/web/src/engine/websites/PerfScan.ts b/web/src/engine/websites/PerfScan.ts
index f988ad90fa..755216ccd2 100644
--- a/web/src/engine/websites/PerfScan.ts
+++ b/web/src/engine/websites/PerfScan.ts
@@ -1,16 +1,8 @@
 import { Tags } from '../Tags';
 import icon from './PerfScan.webp';
-import { DecoratableMangaScraper } from '../providers/MangaPlugin';
-import * as HeamCMS from './decorators/HeanCMS';
+import { HeanCMS } from './templates/HeanCMS';
 
-const apiUrl = 'https://api.perf-scan.fr';
-
-@HeamCMS.MangaCSS(/^{origin}\/series\/[^/]+$/, apiUrl)
-@HeamCMS.MangasMultiPageAJAX(apiUrl)
-@HeamCMS.ChaptersSinglePageAJAXv2(apiUrl)
-@HeamCMS.PagesSinglePageAJAX(apiUrl)
-@HeamCMS.ImageAjax()
-export default class extends DecoratableMangaScraper {
+export default class extends HeanCMS {
 
     public constructor() {
         super('perfscan', 'Perf Scan', 'https://perf-scan.fr', Tags.Media.Manga, Tags.Media.Manhwa, Tags.Media.Manhua, Tags.Media.Novel, Tags.Language.French, Tags.Source.Scanlator);
diff --git a/web/src/engine/websites/decorators/HeanCMS.ts b/web/src/engine/websites/decorators/HeanCMS.ts
deleted file mode 100644
index 112cf0df2e..0000000000
--- a/web/src/engine/websites/decorators/HeanCMS.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-import { Exception } from '../../Error';
-import { FetchJSON } from '../../platform/FetchProvider';
-import { type MangaScraper, type MangaPlugin, Manga, Chapter, Page } from '../../providers/MangaPlugin';
-import type { Priority } from '../../taskpool/TaskPool';
-import * as Common from './Common';
-import { WebsiteResourceKey as R } from '../../../i18n/ILocale';
-
-// TODO: Add Novel support
-
-type APIMangaV1 = {
-    title: string
-    id: number,
-    series_type: 'Comic' | 'Novel',
-    series_slug: string,
-    seasons?: APISeason[]
-}
-
-type APIResult<T> = {
-    data: T
-}
-
-type APISeason = {
-    index: number,
-    chapters: APIChapter[]
-}
-
-type APIChapter = {
-    index: string,
-    id: number,
-    chapter_name: string,
-    chapter_title: string,
-    chapter_slug: string,
-}
-
-type APIPages = {
-    chapter_type: 'Comic' | 'Novel',
-    paywall: boolean,
-    data: string[] | string
-    chapter: {
-        chapter_type: 'Comic' | 'Novel',
-        storage: string,
-        chapter_data?: {
-            images: string[]
-        }
-
-    }
-}
-
-type MangaOrChapterId = {
-    id: string,
-    slug: string
-}
-
-type PageType = {
-    type: 'Comic' | 'Novel'
-}
-
-/***************************************************
- ******** Manga from URL Extraction Methods ********
- ***************************************************/
-
-/**
- * An extension method for extracting a single manga from the given {@link url} using the HeanCMS api url {@link apiUrl}.
- * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
- * @param provider - A reference to the {@link MangaPlugin} which shall be assigned as parent for the extracted manga
- * @param url - the manga url
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export async function FetchMangaCSS(this: MangaScraper, provider: MangaPlugin, url: string, apiUrl: string): Promise<Manga> {
-    const slug = new URL(url).pathname.split('/').at(-1);
-    const { title, series_slug, id } = await FetchJSON<APIMangaV1>(new Request(new URL(`${apiUrl}/series/${slug}`)));
-    return new Manga(this, provider, JSON.stringify({
-        id: id.toString(),
-        slug: series_slug
-    }), title);
-}
-
-/**
- * An extension method for extracting a single manga from any url using the HeanCMS api url {@link apiUrl}.
- * @param pattern - An expression to check if a manga can be extracted from an url or not, it may contain the placeholders `{origin}` and `{hostname}` which will be replaced with the corresponding parameters based on the website's base URL
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export function MangaCSS(pattern: RegExp, apiURL: string) {
-    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
-        Common.ThrowOnUnsupportedDecoratorContext(context);
-        return class extends ctor {
-            public ValidateMangaURL(this: MangaScraper, url: string): boolean {
-                const source = pattern.source.replaceAll('{origin}', this.URI.origin).replaceAll('{hostname}', this.URI.hostname);
-                return new RegExp(source, pattern.flags).test(url);
-            }
-            public async FetchManga(this: MangaScraper, provider: MangaPlugin, url: string): Promise<Manga> {
-                return FetchMangaCSS.call(this, provider, url, apiURL);
-            }
-        };
-    };
-}
-
-/***********************************************
- ******** Manga List Extraction Methods ********
- ***********************************************/
-
-/**
- * An extension method for extracting multiple mangas using the HeanCMS api url {@link apiUrl}.
- * The range begins with 1 and is incremented until no more new mangas can be extracted.
- * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
- * @param provider - A reference to the {@link MangaPlugin} which shall be assigned as parent for the extracted mangas
- * @param apiUrl - The url of the HeanCMS api for the website
- * @param throttle - A delay [ms] for each request (only required for rate-limited websites)
- */
-export async function FetchMangasMultiPageAJAX(this: MangaScraper, provider: MangaPlugin, apiUrl: string, throttle = 0): Promise<Manga[]> {
-    const mangaList: Manga[] = [];
-
-    for (const adult of [true, false]) { //there is no "dont care if adult or not flag" on "new" api, and old dont care about the flag
-        for (let page = 1, run = true; run; page++) {
-            const mangas = await GetMangaFromPage.call(this, provider, page, apiUrl, adult);
-            mangas.length > 0 ? mangaList.push(...mangas) : run = false;
-            await new Promise(resolve => setTimeout(resolve, throttle));
-        }
-    }
-    return mangaList.distinct();//filter in case of old api
-}
-async function GetMangaFromPage(this: MangaScraper, provider: MangaPlugin, page: number, apiUrl: string, adult: boolean): Promise<Manga[]> {
-    const request = new Request(new URL(`${apiUrl}/query?perPage=100&page=${page}&adult=${adult}`));
-    const { data } = await FetchJSON<APIResult<APIMangaV1[]>>(request);
-    return !data.length ? [] : data.map((manga) => new Manga(this, provider, JSON.stringify({ id: manga.id.toString(), slug: manga.series_slug }), manga.title));
-
-}
-
-/**
- * A class decorator that adds the ability to extract multiple mangas from a range of pages using the HeanCMS api url {@link apiUrl}.
- * @param apiUrl - The url of the HeanCMS api for the website
- * @param throttle - A delay [ms] for each request (only required for rate-limited websites)
- */
-export function MangasMultiPageAJAX(apiUrl: string, throttle = 0) {
-    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
-        Common.ThrowOnUnsupportedDecoratorContext(context);
-        return class extends ctor {
-            public async FetchMangas(this: MangaScraper, provider: MangaPlugin): Promise<Manga[]> {
-                return FetchMangasMultiPageAJAX.call(this, provider, apiUrl, throttle);
-            }
-        };
-    };
-}
-
-/*************************************************
- ******** Chapter List Extraction Methods ********
- *************************************************/
-
-/**
- * An extension method for extracting all chapters for the given {@link manga} using the HeanCMS api url {@link apiUrl}.
- * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
- * @param manga - A reference to the {@link Manga} which shall be assigned as parent for the extracted chapters
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export async function FetchChaptersSinglePageAJAXv1(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
-    try {
-        const mangaslug = (JSON.parse(manga.Identifier) as MangaOrChapterId).slug;
-        const request = new Request(new URL(`${apiUrl}/series/${mangaslug}`));
-        const { seasons } = await FetchJSON<APIMangaV1>(request);
-        return seasons.reduce(async (accumulator: Promise<Chapter[]>, season) => {
-            const chapters = season.chapters.map(chapter => {
-                const id = JSON.stringify({
-                    id: chapter.id.toString(),
-                    slug: chapter.chapter_slug
-                });
-                const title = `${seasons.length > 1 ? 'S' + season.index : ''} ${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim();
-                return new Chapter(this, manga, id, title);
-            });
-            (await accumulator).concat(...chapters);
-            return accumulator;
-        }, Promise.resolve<Chapter[]>([]));
-
-    } catch {
-        return [];
-    }
-}
-
-/**
- * A class decorator that adds the ability to extract all chapters for a given manga from this website using the HeanCMS api url {@link apiUrl}.
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export function ChaptersSinglePageAJAXv1(apiUrl: string) {
-    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
-        Common.ThrowOnUnsupportedDecoratorContext(context);
-        return class extends ctor {
-            public async FetchChapters(this: MangaScraper, manga: Manga): Promise<Chapter[]> {
-                return FetchChaptersSinglePageAJAXv1.call(this, manga, apiUrl);
-            }
-        };
-    };
-}
-
-/**
- * An extension method for extracting all chapters for the given {@link manga} using the HeanCMS api url {@link apiUrl}.
- * @param this - A reference to the {@link MangaScraper} instance which will be used as context for this method
- * @param manga - A reference to the {@link Manga} which shall be assigned as parent for the extracted chapters
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export async function FetchChaptersSinglePageAJAXv2(this: MangaScraper, manga: Manga, apiUrl: string): Promise<Chapter[]> {
-    const mangaid: MangaOrChapterId = JSON.parse(manga.Identifier);
-    const { data } = await FetchJSON<APIResult<APIChapter[]>>(new Request(new URL(`${apiUrl}/chapter/query?series_id=${mangaid.id}&perPage=9999&page=1`)));
-    return data.map(chapter => new Chapter(this, manga, JSON.stringify({
-        id: chapter.id.toString(),
-        slug: chapter.chapter_slug,
-    }), `${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim()));
-
-}
-
-/**
- * A class decorator that adds the ability to extract all chapters for a given manga from this website using the HeanCMS api url {@link apiUrl}.
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export function ChaptersSinglePageAJAXv2(apiUrl: string) {
-    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
-        Common.ThrowOnUnsupportedDecoratorContext(context);
-        return class extends ctor {
-            public async FetchChapters(this: MangaScraper, manga: Manga): Promise<Chapter[]> {
-                return FetchChaptersSinglePageAJAXv2.call(this, manga, apiUrl);
-            }
-        };
-    };
-}
-
-/**********************************************
- ******** Page List Extraction Methods ********
- **********************************************/
-/**
- * An extension method for extracting all pages for the given {@link chapter} using the HeanCMS api url {@link apiUrl}.
- * @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 apiUrl - The url of the HeanCMS api for the website
- */
-export async function FetchPagesSinglePageAJAX(this: MangaScraper, chapter: Chapter, apiUrl: string): Promise<Page<PageType>[]> {
-    const chapterid: MangaOrChapterId = JSON.parse(chapter.Identifier);
-    const mangaid: MangaOrChapterId = JSON.parse(chapter.Parent.Identifier);
-    const data = await FetchJSON<APIPages>(new Request(new URL(`${apiUrl}/chapter/${mangaid.slug}/${chapterid.slug}`)));
-
-    if (data.paywall) {
-        throw new Exception(R.Plugin_Common_Chapter_UnavailableError);
-    }
-
-    if (data.chapter.chapter_type.toLowerCase() === 'novel') {
-        throw new Exception(R.Plugin_HeanCMS_ErrorNovelsNotSupported);
-    }
-
-    const listImages = data.data && Array.isArray(data.data) ? data.data as string[] : data.chapter.chapter_data.images;
-    return listImages.map(image => new Page<PageType>(this, chapter, ComputePageUrl(image, data.chapter.storage, apiUrl), { type: data.chapter.chapter_type }));
-}
-
-/**
- * A class decorator that adds the ability to extract all pages for a given chapter using the HeanCMS api url {@link apiUrl}.
- * @param apiUrl - The url of the HeanCMS api for the website
- */
-export function PagesSinglePageAJAX(apiUrl: string) {
-    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
-        Common.ThrowOnUnsupportedDecoratorContext(context);
-        return class extends ctor {
-            public async FetchPages(this: MangaScraper, chapter: Chapter): Promise<Page<PageType>[]> {
-                return FetchPagesSinglePageAJAX.call(this, chapter, apiUrl);
-            }
-        };
-    };
-}
-
-/***********************************************
- ******** Image Data Extraction Methods ********
- ***********************************************/
-
-/**
- * An extension method to get the image data for the given {@link page} according to an XHR based-approach.
- * @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)
- * @param deProxifyLink - Remove common image proxies (default false)
- */
-export async function FetchImageAjax(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal, detectMimeType = false, deProxifyLink = true): Promise<Blob> {
-    if (page.Parameters?.type === 'Comic') {
-        return Common.FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
-    } else throw new Exception(R.Plugin_HeanCMS_ErrorNovelsNotSupported);
-}
-
-/**
- * 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)
- * @param deProxifyLink - Remove common image proxies (default false)
- */
-export function ImageAjax(detectMimeType = false, deProxifyLink = true) {
-    return function DecorateClass<T extends Common.Constructor>(ctor: T, context?: ClassDecoratorContext): T {
-        Common.ThrowOnUnsupportedDecoratorContext(context);
-        return class extends ctor {
-            public async FetchImage(this: MangaScraper, page: Page, priority: Priority, signal: AbortSignal): Promise<Blob> {
-                return FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
-            }
-        };
-    };
-}
-/**
- * Compute the image full URL for HeanHMS based websites
- * @param image - A string containing the full url ( for {@link storage} "s3") or the pathname ( for {@link storage} "local"))
- * @param storage - As string representing the type of storage used : "s3" or "local"
- * @param apiUrl - The url of the HeanCMS api for the website *
- */
-function ComputePageUrl(image: string, storage: string, apiUrl: string): URL {
-    switch (storage) {
-        case "s3": return new URL(image);
-        case "local": return new URL(`${apiUrl}/${image}`);
-    }
-}
diff --git a/web/src/engine/websites/templates/HeanCMS.ts b/web/src/engine/websites/templates/HeanCMS.ts
new file mode 100644
index 0000000000..fd32576641
--- /dev/null
+++ b/web/src/engine/websites/templates/HeanCMS.ts
@@ -0,0 +1,125 @@
+import { Exception } from '../../Error';
+import { FetchJSON } from '../../platform/FetchProvider';
+import {type MangaPlugin, Manga, Chapter, Page, DecoratableMangaScraper } from '../../providers/MangaPlugin';
+import type { Priority } from '../../taskpool/TaskPool';
+import * as Common from '../decorators/Common';
+import { WebsiteResourceKey as R } from '../../../i18n/ILocale';
+
+// TODO: Add Novel support
+
+type APIManga = {
+    title: string
+    id: number,
+    series_type: 'Comic' | 'Novel',
+    series_slug: string,
+}
+
+type APIResult<T> = {
+    data: T
+}
+
+type APIChapter = {
+    index: string,
+    id: number,
+    chapter_name: string,
+    chapter_title: string,
+    chapter_slug: string,
+}
+
+type APIPages = {
+    chapter_type: 'Comic' | 'Novel',
+    paywall: boolean,
+    data: string[] | string
+    chapter: {
+        chapter_type: 'Comic' | 'Novel',
+        storage: string,
+        chapter_data?: {
+            images: string[]
+        }
+
+    }
+}
+
+type APIMediaID = {
+    id: string,
+    slug: string
+}
+
+type PageType = {
+    type: 'Comic' | 'Novel'
+}
+
+export class HeanCMS extends DecoratableMangaScraper {
+
+    protected apiUrl = this.URI.origin.replace('://', '://api.');
+
+    public override ValidateMangaURL(url: string): boolean {
+        return new RegExpSafe(`^${this.URI.origin}/series/[^/]+$`).test(url);
+    }
+
+    public override async FetchManga(provider: MangaPlugin, url: string): Promise<Manga> {
+        const slug = new URL(url).pathname.split('/').at(-1);
+        const { title, series_slug, id } = await FetchJSON<APIManga>(new Request(new URL(`${this.apiUrl}/series/${slug}`)));
+        return new Manga(this, provider, JSON.stringify({
+            id: id.toString(),
+            slug: series_slug
+        }), title);
+    }
+
+    public override async FetchMangas(provider: MangaPlugin): Promise<Manga[]> {
+        const mangaList: Manga[] = [];
+        for (const adult of [true, false]) { //adult flag mean ONLY adult, false mean ONLY all ages... Talk about stupid. Old api ignore that flag
+            for (let page = 1, run = true; run; page++) {
+                const mangas = await this.GetMangaFromPage(provider, page, adult);
+                mangas.length > 0 ? mangaList.push(...mangas) : run = false;
+            }
+        }
+        return mangaList.distinct();//filter in case of old api
+    }
+
+    private async GetMangaFromPage(provider: MangaPlugin, page: number, adult: boolean): Promise<Manga[]> {
+        const request = new Request(new URL(`${this.apiUrl}/query?perPage=100&page=${page}&adult=${adult}`));
+        const { data } = await FetchJSON<APIResult<APIManga[]>>(request);
+        return !data.length ? [] : data.map((manga) => new Manga(this, provider, JSON.stringify({ id: manga.id.toString(), slug: manga.series_slug }), manga.title));
+    }
+
+    public override async FetchChapters(manga: Manga): Promise<Chapter[]> {
+        const mangaid: APIMediaID = JSON.parse(manga.Identifier);
+        const { data } = await FetchJSON<APIResult<APIChapter[]>>(new Request(new URL(`${this.apiUrl}/chapter/query?series_id=${mangaid.id}&perPage=9999&page=1`)));
+        return data.map(chapter => new Chapter(this, manga, JSON.stringify({
+            id: chapter.id.toString(),
+            slug: chapter.chapter_slug,
+        }), `${chapter.chapter_name} ${chapter.chapter_title || ''}`.trim()));
+    }
+
+    public override async FetchPages(chapter: Chapter): Promise<Page<PageType>[]> {
+        const chapterid: APIMediaID = JSON.parse(chapter.Identifier);
+        const mangaid: APIMediaID = JSON.parse(chapter.Parent.Identifier);
+        const data = await FetchJSON<APIPages>(new Request(new URL(`${this.apiUrl}/chapter/${mangaid.slug}/${chapterid.slug}`)));
+
+        if (data.paywall) {
+            throw new Exception(R.Plugin_Common_Chapter_UnavailableError);
+        }
+
+        if (data.chapter.chapter_type.toLowerCase() === 'novel') {
+            throw new Exception(R.Plugin_HeanCMS_ErrorNovelsNotSupported);
+        }
+        //old API vs new
+        const listImages = data.data && Array.isArray(data.data) ? data.data as string[] : data.chapter.chapter_data.images;
+        return listImages.map(image => new Page<PageType>(this, chapter, this.ComputePageUrl(image, data.chapter.storage), { type: data.chapter.chapter_type }));
+    }
+
+    private ComputePageUrl(image: string, storage: string): URL {
+        switch (storage) {
+            case "s3": return new URL(image);
+            case "local": return new URL(`${this.apiUrl}/${image}`);
+        }
+    }
+
+    public override async FetchImage(page: Page<PageType>, priority: Priority, signal: AbortSignal, detectMimeType = true, deProxifyLink = true): Promise<Blob> {
+        if (page.Parameters?.type === 'Comic') {
+            return Common.FetchImageAjax.call(this, page, priority, signal, detectMimeType, deProxifyLink);
+        } else throw new Exception(R.Plugin_HeanCMS_ErrorNovelsNotSupported);
+    }
+
+}

From b794e4d240acecb8bd600ed75d36520b3c817baa Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 13 Oct 2024 11:14:56 +0200
Subject: [PATCH 26/31] Update PerfScan_e2e.ts

---
 web/src/engine/websites/PerfScan_e2e.ts | 24 ------------------------
 1 file changed, 24 deletions(-)

diff --git a/web/src/engine/websites/PerfScan_e2e.ts b/web/src/engine/websites/PerfScan_e2e.ts
index f1e4d4344b..542b1d01ba 100644
--- a/web/src/engine/websites/PerfScan_e2e.ts
+++ b/web/src/engine/websites/PerfScan_e2e.ts
@@ -24,27 +24,3 @@ const ComicConfig = {
 
 const ComicFixture = new TestFixture(ComicConfig);
 describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
-
-const NovelConfig = {
-    plugin: {
-        id: 'perfscan',
-        title: 'Perf Scan'
-    },
-    container: {
-        url: 'https://perf-scan.fr/series/demonic-emperor-novel',
-        id: JSON.stringify({ id: '17', slug: 'demonic-emperor-novel' }),
-        title: 'Demonic emperor - Novel'
-    },
-    child: {
-        id: JSON.stringify({ id: '28668', slug: 'chapitre-492' }),
-        title: 'Chapitre 492'
-    },
-    entry: {
-        index: 0,
-        size: 789_130,
-        type: 'image/png'
-    }
-};
-
-const NovelFixture = new TestFixture(NovelConfig);
-describe(NovelFixture.Name, async () => (await NovelFixture.Connect()).AssertWebsite());
\ No newline at end of file

From 26139912c0ab25e4d892bc76b6a7b702169c489c Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 27 Oct 2024 10:46:54 +0100
Subject: [PATCH 27/31] update tests

---
 web/src/engine/websites/OmegaScans_e2e.ts | 4 +---
 web/src/engine/websites/PerfScan_e2e.ts   | 4 +---
 2 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/web/src/engine/websites/OmegaScans_e2e.ts b/web/src/engine/websites/OmegaScans_e2e.ts
index 163a757026..c2088e5386 100644
--- a/web/src/engine/websites/OmegaScans_e2e.ts
+++ b/web/src/engine/websites/OmegaScans_e2e.ts
@@ -1,4 +1,3 @@
-import { describe } from 'vitest';
 import { TestFixture } from '../../../test/WebsitesFixture';
 
 const ComicConfig = {
@@ -22,5 +21,4 @@ const ComicConfig = {
     }
 };
 
-const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
\ No newline at end of file
+new TestFixture(ComicConfig).AssertWebsite();
diff --git a/web/src/engine/websites/PerfScan_e2e.ts b/web/src/engine/websites/PerfScan_e2e.ts
index 542b1d01ba..dd47595b19 100644
--- a/web/src/engine/websites/PerfScan_e2e.ts
+++ b/web/src/engine/websites/PerfScan_e2e.ts
@@ -1,4 +1,3 @@
-import { describe } from 'vitest';
 import { TestFixture } from '../../../test/WebsitesFixture';
 
 const ComicConfig = {
@@ -22,5 +21,4 @@ const ComicConfig = {
     }
 };
 
-const ComicFixture = new TestFixture(ComicConfig);
-describe(ComicFixture.Name, async () => (await ComicFixture.Connect()).AssertWebsite());
+new TestFixture(ComicConfig).AssertWebsite();

From 5ed360178992c52bd778f62687e953627d65c492 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 3 Nov 2024 13:37:05 +0100
Subject: [PATCH 28/31] add base class tests

---
 web/src/engine/websites/templates/HeanCMS_e2e.ts | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 web/src/engine/websites/templates/HeanCMS_e2e.ts

diff --git a/web/src/engine/websites/templates/HeanCMS_e2e.ts b/web/src/engine/websites/templates/HeanCMS_e2e.ts
new file mode 100644
index 0000000000..73b905c3cc
--- /dev/null
+++ b/web/src/engine/websites/templates/HeanCMS_e2e.ts
@@ -0,0 +1,3 @@
+import '../ModeScanlator_e2e';
+import '../OmegaScans_e2e';
+import '../PerfScan_e2e';
\ No newline at end of file

From 9a87cb97ef76f7eeac8843d6c284d1833e71d613 Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Nov 2024 21:37:29 +0100
Subject: [PATCH 29/31] remove modescanlator

---
 web/src/engine/websites/ModeScanlator.ts     |  15 -----------
 web/src/engine/websites/ModeScanlator.webp   | Bin 1986 -> 0 bytes
 web/src/engine/websites/ModeScanlator_e2e.ts |  25 -------------------
 web/src/engine/websites/_index.ts            |   1 -
 4 files changed, 41 deletions(-)
 delete mode 100644 web/src/engine/websites/ModeScanlator.ts
 delete mode 100644 web/src/engine/websites/ModeScanlator.webp
 delete mode 100644 web/src/engine/websites/ModeScanlator_e2e.ts

diff --git a/web/src/engine/websites/ModeScanlator.ts b/web/src/engine/websites/ModeScanlator.ts
deleted file mode 100644
index d53b5177f2..0000000000
--- a/web/src/engine/websites/ModeScanlator.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Tags } from '../Tags';
-import icon from './ModeScanlator.webp';
-import { HeanCMS } from './templates/HeanCMS';
-
-export default class extends HeanCMS {
-
-    public constructor() {
-        super('modescanlator', `Mode Scanlator`, 'https://site.modescanlator.net', Tags.Language.Portuguese, Tags.Media.Manga, Tags.Media.Manhua, Tags.Media.Manhwa, Tags.Source.Scanlator);
-        this.apiUrl = this.URI.origin.replace('site', 'api');
-    }
-
-    public override get Icon() {
-        return icon;
-    }
-}
\ No newline at end of file
diff --git a/web/src/engine/websites/ModeScanlator.webp b/web/src/engine/websites/ModeScanlator.webp
deleted file mode 100644
index 97464ba5e5d5e937b0ac64d356ba49996c98c6b9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1986
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
UNECP;H3Ny;x4Xi~10=8j0Ps@h7XSbN

diff --git a/web/src/engine/websites/ModeScanlator_e2e.ts b/web/src/engine/websites/ModeScanlator_e2e.ts
deleted file mode 100644
index 06e976aeaf..0000000000
--- a/web/src/engine/websites/ModeScanlator_e2e.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { TestFixture, type Config } from '../../../test/WebsitesFixture';
-
-const config: Config = {
-    plugin: {
-        id: 'modescanlator',
-        title: 'Mode Scanlator'
-    },
-    container: {
-        url: 'https://site.modescanlator.net/series/uma-lenda-do-vento',
-        id: JSON.stringify({ id: '36', slug: 'uma-lenda-do-vento' }),
-        title: 'Uma Lenda do Vento',
-        timeout: 10000
-    },
-    child: {
-        id: JSON.stringify({ id: '2420', slug: 'capitulo-126' }),
-        title: 'Capítulo 126',
-    },
-    entry: {
-        index: 0,
-        size: 1_222_675,
-        type: 'image/jpeg'
-    }
-};
-
-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 970065c970..6be3e608cc 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -449,7 +449,6 @@ export { default as MindaFanSub } from './MindaFanSub';
 export { default as MiniTwoScan } from './MiniTwoScan';
 export { default as mkzhan } from './mkzhan';
 export { default as MMFenix } from './MMFenix';
-export { default as ModeScanlator } from './ModeScanlator';
 export { default as MonochromeScans } from './MonochromeScans';
 export { default as MonoManga } from './MonoManga';
 export { default as MonzeeKomik } from './MonzeeKomik';

From aabfe5434d4bd14d862de0aae84fd076d92cc94f Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Mon, 11 Nov 2024 20:45:44 +0000
Subject: [PATCH 30/31] Update HeanCMS_e2e.ts

---
 web/src/engine/websites/templates/HeanCMS_e2e.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/web/src/engine/websites/templates/HeanCMS_e2e.ts b/web/src/engine/websites/templates/HeanCMS_e2e.ts
index 73b905c3cc..2e7a936502 100644
--- a/web/src/engine/websites/templates/HeanCMS_e2e.ts
+++ b/web/src/engine/websites/templates/HeanCMS_e2e.ts
@@ -1,3 +1,2 @@
-import '../ModeScanlator_e2e';
 import '../OmegaScans_e2e';
-import '../PerfScan_e2e';
\ No newline at end of file
+import '../PerfScan_e2e';

From a224b215ef1a121c4385a29e7240a2d1a7a26c2c Mon Sep 17 00:00:00 2001
From: MikeZeDev <MikeZeDev@users.noreply.github.com>
Date: Sun, 15 Dec 2024 12:11:25 +0100
Subject: [PATCH 31/31] Update _index.ts

---
 web/src/engine/websites/_index.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/web/src/engine/websites/_index.ts b/web/src/engine/websites/_index.ts
index 9f72c71025..a123cfc79b 100755
--- a/web/src/engine/websites/_index.ts
+++ b/web/src/engine/websites/_index.ts
@@ -491,6 +491,7 @@ export { default as Noromax } from './Noromax';
 export { default as NovelMic } from './NovelMic';
 export { default as NoxScans } from './NoxScans';
 export { default as OlympusScanlation } from './OlympusScanlation';
+export { default as OmegaScans } from './OmegaScans';
 export { default as Opiatoon } from './Opiatoon';
 export { default as Oremanga } from './Oremanga';
 export { default as OrigamiOrpheans } from './OrigamiOrpheans';
@@ -503,6 +504,7 @@ export { default as PCNet } from './PCNet';
 export { default as PeanuToon } from './PeanuToon';
 export { default as PelaTeam } from './PelaTeam';
 export { default as Penlab } from './Penlab';
+export { default as PerfScan } from './PerfScan';
 export { default as PhenixScans } from './PhenixScans';
 export { default as PhoenixScansIT } from './PhoenixScansIT';
 export { default as Piccoma } from './Piccoma';