From 5d78e42a6d0b9a7d41a8591e5e610b96edf991ce Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:43:52 +0000 Subject: [PATCH 1/2] fix(foxaholic): resolve covers as Data URIs and clean up cover extraction (v2.2.1) --- plugins/multisrc/madara/sources.json | 4 +- plugins/multisrc/madara/template.ts | 95 ++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/plugins/multisrc/madara/sources.json b/plugins/multisrc/madara/sources.json index d35784360..6e7b38526 100644 --- a/plugins/multisrc/madara/sources.json +++ b/plugins/multisrc/madara/sources.json @@ -221,7 +221,9 @@ "sourceSite": "https://www.foxaholic.com/", "sourceName": "Foxaholic", "options": { - "useNewChapterEndpoint": true + "useNewChapterEndpoint": true, + "resolveCoverDataUri": true, + "versionIncrements": 1 } }, { diff --git a/plugins/multisrc/madara/template.ts b/plugins/multisrc/madara/template.ts index 940c1ceef..529340baf 100644 --- a/plugins/multisrc/madara/template.ts +++ b/plugins/multisrc/madara/template.ts @@ -19,6 +19,7 @@ type MadaraOptions = { customJs?: string; hasLocked?: boolean; browserHeaders?: boolean; + resolveCoverDataUri?: boolean; }; export type MadaraMetadata = { @@ -139,6 +140,27 @@ export class MadaraPlugin implements Plugin.PluginBase { return $; } + extractCover(image: Cheerio): string { + const candidate = + image.attr('data-lazy-src') || + image.attr('data-src') || + image.attr('data-cfsrc') || + image.attr('data-srcset')?.split(' ')[0] || + image.attr('data-lazy-srcset')?.split(' ')[0] || + image.attr('srcset')?.split(' ')[0]; + + if (candidate && !candidate.includes('dflazy')) { + return candidate; + } + + const src = image.attr('src'); + if (src && !src.includes('dflazy')) { + return src; + } + + return defaultCover; + } + parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; @@ -154,12 +176,10 @@ export class MadaraPlugin implements Plugin.PluginBase { loadedCheerio(element).find('.post-title').find('a').attr('href') || ''; if (!novelName || !novelUrl) return; - const image = loadedCheerio(element).find('img'); - const novelCover = - image.attr('data-src') || - image.attr('src') || - image.attr('data-lazy-srcset') || - defaultCover; + const novelCover = this.extractCover( + loadedCheerio(element).find('img'), + ); + const novel: Plugin.NovelItem = { name: novelName, cover: novelCover, @@ -172,6 +192,57 @@ export class MadaraPlugin implements Plugin.PluginBase { return novels; } + async resolveCoverAsDataUri(url: string): Promise { + if (!url || !url.startsWith('http') || url.startsWith('data:')) return url; + try { + const controller = + typeof AbortController !== 'undefined' ? new AbortController() : null; + const timeout = controller + ? setTimeout(() => controller.abort(), 4000) + : null; + const res = await fetchApi(url, { + headers: this.requestHeaders, + ...(controller ? { signal: controller.signal } : {}), + }); + if (timeout) clearTimeout(timeout); + if (!res.ok) return url; + const blob = await res.blob(); + return await new Promise(resolve => { + const FileReaderClass = + typeof FileReader !== 'undefined' + ? FileReader + : (globalThis as any).FileReader; + if (!FileReaderClass) return resolve(url); + const fr = new FileReaderClass(); + fr.onloadend = () => { + if (typeof fr.result === 'string') { + resolve(fr.result); + } else { + resolve(url); + } + }; + fr.onerror = () => resolve(url); + fr.readAsDataURL(blob); + }); + } catch { + return url; + } + } + + async resolveNovelsCovers( + novels: Plugin.NovelItem[], + ): Promise { + if (!this.options?.resolveCoverDataUri) return novels; + await Promise.all( + novels.map(async novel => { + if (novel.cover && novel.cover.startsWith('http')) { + novel.cover = await this.resolveCoverAsDataUri(novel.cover); + } + }), + ); + return novels; + } + async popularNovels( pageNo: number, { @@ -189,7 +260,8 @@ export class MadaraPlugin implements Plugin.PluginBase { else if (filters[key].value) url += `&${key}=${filters[key].value}`; } const loadedCheerio = await this.getCheerio(url, pageNo != 1); - return this.parseNovels(loadedCheerio); + const novels = this.parseNovels(loadedCheerio); + return await this.resolveNovelsCovers(novels); } async parseNovel(novelPath: string): Promise { @@ -205,11 +277,7 @@ export class MadaraPlugin implements Plugin.PluginBase { '', }; - novel.cover = - loadedCheerio('.summary_image > a > img').attr('data-lazy-src') || - loadedCheerio('.summary_image > a > img').attr('data-src') || - loadedCheerio('.summary_image > a > img').attr('src') || - defaultCover; + novel.cover = this.extractCover(loadedCheerio('.summary_image > a > img')); loadedCheerio('.post-content_item, .post-content').each(function () { const detailName = loadedCheerio(this).find('h5').text().trim(); @@ -446,7 +514,8 @@ export class MadaraPlugin implements Plugin.PluginBase { encodeURIComponent(searchTerm) + '&post_type=wp-manga'; const loadedCheerio = await this.getCheerio(url, true); - return this.parseNovels(loadedCheerio); + const novels = this.parseNovels(loadedCheerio); + return await this.resolveNovelsCovers(novels); } parseData = (date: string) => { From 4ff68a2fc4e2e03b2b28aedd863adf3b8612a5f4 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:07:05 +0000 Subject: [PATCH 2/2] fix(madara): iterate candidates in extractCover and keep timeout active in finally --- plugins/multisrc/madara/template.ts | 37 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/plugins/multisrc/madara/template.ts b/plugins/multisrc/madara/template.ts index 529340baf..466c8c9be 100644 --- a/plugins/multisrc/madara/template.ts +++ b/plugins/multisrc/madara/template.ts @@ -141,21 +141,20 @@ export class MadaraPlugin implements Plugin.PluginBase { } extractCover(image: Cheerio): string { - const candidate = - image.attr('data-lazy-src') || - image.attr('data-src') || - image.attr('data-cfsrc') || - image.attr('data-srcset')?.split(' ')[0] || - image.attr('data-lazy-srcset')?.split(' ')[0] || - image.attr('srcset')?.split(' ')[0]; - - if (candidate && !candidate.includes('dflazy')) { - return candidate; - } - - const src = image.attr('src'); - if (src && !src.includes('dflazy')) { - return src; + const candidates = [ + image.attr('data-lazy-src'), + image.attr('data-src'), + image.attr('data-cfsrc'), + image.attr('data-srcset')?.split(' ')[0], + image.attr('data-lazy-srcset')?.split(' ')[0], + image.attr('srcset')?.split(' ')[0], + image.attr('src'), + ]; + + for (const candidate of candidates) { + if (candidate && !candidate.includes('dflazy')) { + return candidate; + } } return defaultCover; @@ -194,17 +193,15 @@ export class MadaraPlugin implements Plugin.PluginBase { async resolveCoverAsDataUri(url: string): Promise { if (!url || !url.startsWith('http') || url.startsWith('data:')) return url; + let timeout: ReturnType | null = null; try { const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; - const timeout = controller - ? setTimeout(() => controller.abort(), 4000) - : null; + timeout = controller ? setTimeout(() => controller.abort(), 4000) : null; const res = await fetchApi(url, { headers: this.requestHeaders, ...(controller ? { signal: controller.signal } : {}), }); - if (timeout) clearTimeout(timeout); if (!res.ok) return url; const blob = await res.blob(); return await new Promise(resolve => { @@ -226,6 +223,8 @@ export class MadaraPlugin implements Plugin.PluginBase { }); } catch { return url; + } finally { + if (timeout) clearTimeout(timeout); } }