From 9e723c354bee09b51cb0fa904b1ab2ba6d507e00 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sat, 31 Oct 2026 09:17:44 +0700 Subject: [PATCH 1/2] fix(madara): preserve volume order for multi-volume chapter lists (v2.4.0) --- plugins/multisrc/madara/template.ts | 52 +++++++-- scripts/test-madara-order.js | 162 ++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 scripts/test-madara-order.js diff --git a/plugins/multisrc/madara/template.ts b/plugins/multisrc/madara/template.ts index 692f52b76..a8d18f408 100644 --- a/plugins/multisrc/madara/template.ts +++ b/plugins/multisrc/madara/template.ts @@ -58,7 +58,7 @@ export class MadaraPlugin implements Plugin.PluginBase { this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `2.3.${versionIncrements}`; + this.version = `2.4.${versionIncrements}`; this.options = metadata.options; this.filters = metadata.filters; @@ -496,20 +496,17 @@ export class MadaraPlugin implements Plugin.PluginBase { loadedCheerio = parseHTML(html); } - const chapterElements = loadedCheerio('.wp-manga-chapter'); - const totalChapters = chapterElements.length; const defaultDate = dayjs().format('LL'); - - chapterElements.each((chapterIndex, element) => { + const parseChapterEl = (element: AnyNode): Plugin.ChapterItem | null => { const $el = loadedCheerio(element); const $a = $el.find('a'); const chapterUrl = $a.attr('href') || ''; - if (!chapterUrl || chapterUrl === '#') return; + if (!chapterUrl || chapterUrl === '#') return null; const elementClass = (element as Element).attribs?.class || ''; const locked = elementClass.includes('premium-block'); - if (locked && this.hideLocked) return; + if (locked && this.hideLocked) return null; let chapterName = $a.text().trim(); if (locked) { @@ -519,15 +516,50 @@ export class MadaraPlugin implements Plugin.PluginBase { const rawDate = $el.find('span.chapter-release-date').text().trim(); const releaseDate = rawDate ? this.parseData(rawDate) : defaultDate; - chapters.push({ + return { name: chapterName, path: chapterUrl.replace(/https?:\/\/.*?\//, ''), releaseTime: releaseDate || null, - chapterNumber: totalChapters - chapterIndex, + }; + }; + + const volumeElements = loadedCheerio('li.parent.has-child'); + if (volumeElements.length > 0) { + volumeElements.each((_, volEl) => { + const volChapters: Plugin.ChapterItem[] = []; + loadedCheerio(volEl) + .find('.wp-manga-chapter') + .each((_, element) => { + const chap = parseChapterEl(element); + if (chap) volChapters.push(chap); + }); + chapters.push(...volChapters.reverse()); + }); + + const outsideChapters: Plugin.ChapterItem[] = []; + loadedCheerio('.wp-manga-chapter').each((_, element) => { + if (!loadedCheerio(element).parents('li.parent.has-child').length) { + const chap = parseChapterEl(element); + if (chap) outsideChapters.push(chap); + } + }); + if (outsideChapters.length > 0) { + chapters.push(...outsideChapters.reverse()); + } + } else { + const flatChapters: Plugin.ChapterItem[] = []; + loadedCheerio('.wp-manga-chapter').each((_, element) => { + const chap = parseChapterEl(element); + if (chap) flatChapters.push(chap); }); + chapters.push(...flatChapters.reverse()); + } + + chapters.forEach((chapter, index) => { + chapter.chapterNumber = index + 1; }); - novel.chapters = chapters.reverse(); + novel.chapters = chapters; return novel; } diff --git a/scripts/test-madara-order.js b/scripts/test-madara-order.js new file mode 100644 index 000000000..cbd2ce467 --- /dev/null +++ b/scripts/test-madara-order.js @@ -0,0 +1,162 @@ +import assert from 'node:assert/strict'; +import { readFileSync, existsSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; +import { runInNewContext } from 'node:vm'; +import ts from 'typescript'; + +const require = createRequire(import.meta.url); +const source = readFileSync( + new URL('../plugins/english/Foxaholic[madara].ts', import.meta.url), + 'utf8', +); + +const code = ts.transpileModule(source, { + compilerOptions: { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.CommonJS, + }, +}).outputText; + +function loadPlugin(mockHtml) { + const exports = {}; + const dayjs = require('dayjs'); + const dayjsFn = dayjs.default || dayjs; + if (!dayjsFn.default) dayjsFn.default = dayjsFn; + const stubs = { + '@libs/fetch': { + fetchApi: async url => { + return { + ok: true, + status: 200, + url, + text: async () => mockHtml, + }; + }, + }, + '@libs/storage': { storage: { get: () => false } }, + '@libs/filterInputs': { FilterTypes: {} }, + '@libs/novelStatus': { + NovelStatus: { + Ongoing: 'Ongoing', + Completed: 'Completed', + Unknown: 'Unknown', + }, + }, + '@libs/defaultCover': { defaultCover: 'default-cover.png' }, + dayjs: dayjsFn, + cheerio: require('cheerio'), + }; + + runInNewContext(code, { + exports, + require: name => stubs[name] || require(name), + URLSearchParams, + setTimeout, + FormData: class FormData { + append() { + return undefined; + } + }, + }); + + return exports.default; +} + +test('Madara multi-volume chapter ordering', async () => { + const multiVolHtml = ` + + `; + + const plugin = loadPlugin(multiVolHtml); + const novel = await plugin.parseNovel('novel/test/'); + + assert.equal(novel.chapters.length, 6); + assert.equal(novel.chapters[0].name, 'Chapter 1'); + assert.equal(novel.chapters[0].chapterNumber, 1); + assert.equal(novel.chapters[1].name, 'Chapter 2'); + assert.equal(novel.chapters[1].chapterNumber, 2); + assert.equal(novel.chapters[2].name, 'Chapter 3'); + assert.equal(novel.chapters[2].chapterNumber, 3); + assert.equal(novel.chapters[3].name, 'Chapter 4'); + assert.equal(novel.chapters[3].chapterNumber, 4); + assert.equal(novel.chapters[4].name, 'Chapter 5'); + assert.equal(novel.chapters[4].chapterNumber, 5); + assert.equal(novel.chapters[5].name, 'Chapter 6'); + assert.equal(novel.chapters[5].chapterNumber, 6); +}); + +test('Madara chapter order for flat non-volume novel', async () => { + const flatHtml = ` + + `; + const plugin = loadPlugin(flatHtml); + const novel = await plugin.parseNovel('novel/test/'); + + assert.equal(novel.chapters.length, 3); + assert.equal(novel.chapters[0].name, 'Chapter 1'); + assert.equal(novel.chapters[0].chapterNumber, 1); + assert.equal(novel.chapters[1].name, 'Chapter 2'); + assert.equal(novel.chapters[1].chapterNumber, 2); + assert.equal(novel.chapters[2].name, 'Chapter 3'); + assert.equal(novel.chapters[2].chapterNumber, 3); +}); + +test('Madara multi-volume chapter order with real Strange Grief fixture if present', async () => { + const fixturePath = new URL( + '../strange_grief_chapters.html', + import.meta.url, + ); + if (!existsSync(fixturePath)) return; + + const html = readFileSync(fixturePath, 'utf-8'); + const plugin = loadPlugin(html); + const novel = await plugin.parseNovel( + 'novel/strange-grief-wants-to-retire-the-weakest-hunters-hero-dream/', + ); + + assert.equal(novel.chapters.length, 927); + assert.equal(novel.chapters[0].name, 'SS1 - Tino Shade’s Creed'); + assert.equal(novel.chapters[0].chapterNumber, 1); + assert.equal(novel.chapters[4].name, '1-23 - PDF Chapter 1-23 by Redhare404'); + assert.equal(novel.chapters[4].chapterNumber, 5); + assert.equal( + novel.chapters[5].name, + '24-49 - PDF Chapter 24-49 by Redhare404', + ); + assert.equal(novel.chapters[5].chapterNumber, 6); + assert.equal( + novel.chapters[novel.chapters.length - 1].name, + "503.1 - Fox God's Shrine Maiden", + ); + assert.equal(novel.chapters[novel.chapters.length - 1].chapterNumber, 927); +}); From 287affa6a4e72089a0a046e78328ed299b687dc9 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sat, 31 Oct 2026 09:29:50 +0700 Subject: [PATCH 2/2] Delete scripts/test-madara-order.js --- scripts/test-madara-order.js | 162 ----------------------------------- 1 file changed, 162 deletions(-) delete mode 100644 scripts/test-madara-order.js diff --git a/scripts/test-madara-order.js b/scripts/test-madara-order.js deleted file mode 100644 index cbd2ce467..000000000 --- a/scripts/test-madara-order.js +++ /dev/null @@ -1,162 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFileSync, existsSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import { test } from 'node:test'; -import { runInNewContext } from 'node:vm'; -import ts from 'typescript'; - -const require = createRequire(import.meta.url); -const source = readFileSync( - new URL('../plugins/english/Foxaholic[madara].ts', import.meta.url), - 'utf8', -); - -const code = ts.transpileModule(source, { - compilerOptions: { - target: ts.ScriptTarget.ES2020, - module: ts.ModuleKind.CommonJS, - }, -}).outputText; - -function loadPlugin(mockHtml) { - const exports = {}; - const dayjs = require('dayjs'); - const dayjsFn = dayjs.default || dayjs; - if (!dayjsFn.default) dayjsFn.default = dayjsFn; - const stubs = { - '@libs/fetch': { - fetchApi: async url => { - return { - ok: true, - status: 200, - url, - text: async () => mockHtml, - }; - }, - }, - '@libs/storage': { storage: { get: () => false } }, - '@libs/filterInputs': { FilterTypes: {} }, - '@libs/novelStatus': { - NovelStatus: { - Ongoing: 'Ongoing', - Completed: 'Completed', - Unknown: 'Unknown', - }, - }, - '@libs/defaultCover': { defaultCover: 'default-cover.png' }, - dayjs: dayjsFn, - cheerio: require('cheerio'), - }; - - runInNewContext(code, { - exports, - require: name => stubs[name] || require(name), - URLSearchParams, - setTimeout, - FormData: class FormData { - append() { - return undefined; - } - }, - }); - - return exports.default; -} - -test('Madara multi-volume chapter ordering', async () => { - const multiVolHtml = ` - - `; - - const plugin = loadPlugin(multiVolHtml); - const novel = await plugin.parseNovel('novel/test/'); - - assert.equal(novel.chapters.length, 6); - assert.equal(novel.chapters[0].name, 'Chapter 1'); - assert.equal(novel.chapters[0].chapterNumber, 1); - assert.equal(novel.chapters[1].name, 'Chapter 2'); - assert.equal(novel.chapters[1].chapterNumber, 2); - assert.equal(novel.chapters[2].name, 'Chapter 3'); - assert.equal(novel.chapters[2].chapterNumber, 3); - assert.equal(novel.chapters[3].name, 'Chapter 4'); - assert.equal(novel.chapters[3].chapterNumber, 4); - assert.equal(novel.chapters[4].name, 'Chapter 5'); - assert.equal(novel.chapters[4].chapterNumber, 5); - assert.equal(novel.chapters[5].name, 'Chapter 6'); - assert.equal(novel.chapters[5].chapterNumber, 6); -}); - -test('Madara chapter order for flat non-volume novel', async () => { - const flatHtml = ` - - `; - const plugin = loadPlugin(flatHtml); - const novel = await plugin.parseNovel('novel/test/'); - - assert.equal(novel.chapters.length, 3); - assert.equal(novel.chapters[0].name, 'Chapter 1'); - assert.equal(novel.chapters[0].chapterNumber, 1); - assert.equal(novel.chapters[1].name, 'Chapter 2'); - assert.equal(novel.chapters[1].chapterNumber, 2); - assert.equal(novel.chapters[2].name, 'Chapter 3'); - assert.equal(novel.chapters[2].chapterNumber, 3); -}); - -test('Madara multi-volume chapter order with real Strange Grief fixture if present', async () => { - const fixturePath = new URL( - '../strange_grief_chapters.html', - import.meta.url, - ); - if (!existsSync(fixturePath)) return; - - const html = readFileSync(fixturePath, 'utf-8'); - const plugin = loadPlugin(html); - const novel = await plugin.parseNovel( - 'novel/strange-grief-wants-to-retire-the-weakest-hunters-hero-dream/', - ); - - assert.equal(novel.chapters.length, 927); - assert.equal(novel.chapters[0].name, 'SS1 - Tino Shade’s Creed'); - assert.equal(novel.chapters[0].chapterNumber, 1); - assert.equal(novel.chapters[4].name, '1-23 - PDF Chapter 1-23 by Redhare404'); - assert.equal(novel.chapters[4].chapterNumber, 5); - assert.equal( - novel.chapters[5].name, - '24-49 - PDF Chapter 24-49 by Redhare404', - ); - assert.equal(novel.chapters[5].chapterNumber, 6); - assert.equal( - novel.chapters[novel.chapters.length - 1].name, - "503.1 - Fox God's Shrine Maiden", - ); - assert.equal(novel.chapters[novel.chapters.length - 1].chapterNumber, 927); -});