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..466c8c9be 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,26 @@ export class MadaraPlugin implements Plugin.PluginBase { return $; } + extractCover(image: Cheerio): string { + 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; + } + parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; @@ -154,12 +175,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 +191,57 @@ export class MadaraPlugin implements Plugin.PluginBase { return novels; } + 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; + timeout = controller ? setTimeout(() => controller.abort(), 4000) : null; + const res = await fetchApi(url, { + headers: this.requestHeaders, + ...(controller ? { signal: controller.signal } : {}), + }); + 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; + } finally { + if (timeout) clearTimeout(timeout); + } + } + + 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 +259,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 +276,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 +513,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) => {