From 68a5fe2e5d9c79032e3d469e8640d75810c7b97c Mon Sep 17 00:00:00 2001 From: The mystery Date: Sat, 7 Nov 2026 11:01:15 +0530 Subject: [PATCH 1/2] Refactor chapter URL retrieval and parsing methods Refactor getChapterUrls to use flight data for chapters and add static methods for parsing chapter data from flight data and rendered links. --- plugin/js/parsers/WoopreadParser.js | 85 +++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/plugin/js/parsers/WoopreadParser.js b/plugin/js/parsers/WoopreadParser.js index 0c5be59f4..04bf09701 100644 --- a/plugin/js/parsers/WoopreadParser.js +++ b/plugin/js/parsers/WoopreadParser.js @@ -25,20 +25,85 @@ class WoopreadParser extends Parser { } async getChapterUrls(dom) { - const chapterLinks = [...dom.querySelectorAll("main.grow .notranslate .mt-8 .grid-cols-1 a")]; - const chapterTitles = [...dom.querySelectorAll("main.grow .line-clamp-2")]; - - let chapterList = []; - for (let i = 0; i < chapterLinks.length; i++) { - chapterList.push({ - sourceUrl: chapterLinks[i].href, - title: chapterTitles[i].textContent, + // The page only renders the newest chapter group in the DOM (older groups are + // collapsed and not rendered), but the full list is embedded in the Next.js + // flight data (self.__next_f.push). Read the full list from there. + let chapters = WoopreadParser.chaptersFromFlightData(dom); + if (chapters.length === 0) { + // The DOM taken from the open browser tab has already been hydrated: the + // flight data scripts are gone and only the expanded chapter groups exist. + // Re-fetch the raw page, which still contains the full list. + try { + let rawDom = (await HttpClient.wrapFetch(dom.baseURI)).responseXML; + chapters = WoopreadParser.chaptersFromFlightData(rawDom); + } catch (e) { + // fall through to the rendered links + } + } + if (chapters.length === 0) { + return WoopreadParser.chaptersFromRenderedLinks(dom); + } + let base = dom.baseURI || "https://woopread.com/"; + return chapters + .sort((a, b) => a.number - b.number) + .map(c => ({ + sourceUrl: new URL(`/series/${c.novelSlug}/${c.slug}`, base).href, + title: c.title + })); + } + + static chaptersFromFlightData(dom) { + let payload = ""; + for (let script of dom.querySelectorAll("script")) { + let text = script.textContent; + if (!text.includes("__next_f.push")) { + continue; + } + let match = text.match(/__next_f\.push\((\[[\s\S]*\])\)\s*;?\s*$/); + if (match === null) { + continue; + } + try { + let arr = JSON.parse(match[1]); + if (arr.length > 1 && typeof arr[1] === "string") { + payload += arr[1]; + } + } catch (e) { + // ignore chunks that are not plain JSON + } + } + + let novelSlug = (payload.match(/"novelSlug":"([^"]+)"/) || [])[1]; + let listStart = payload.indexOf("\"chapters\":[{"); + if (!novelSlug || listStart < 0) { + return []; + } + let listEnd = payload.indexOf("],\"novelSlug\"", listStart); + let listText = payload.substring(listStart, listEnd > 0 ? listEnd : undefined); + let chapterRegex = /\{"id":"[^"]*","title":"((?:[^"\\]|\\.)*)","number":(\d+),"slug":"((?:[^"\\]|\\.)*)"/g; + let chapters = []; + let m; + while ((m = chapterRegex.exec(listText)) !== null) { + chapters.push({ + title: JSON.parse(`"${m[1]}"`), + number: parseInt(m[2]), + slug: JSON.parse(`"${m[3]}"`), + novelSlug: novelSlug }); } + return chapters; + } - return chapterList.reverse(); + static chaptersFromRenderedLinks(dom) { + // Fallback: only the expanded chapter group(s) are available this way + let links = [...dom.querySelectorAll("main.grow a[href*='/chapter-'], main.grow a[href*='/side-story-']")] + .filter(a => a.querySelector(".truncate") !== null); + return links.map(a => ({ + sourceUrl: a.href, + title: a.querySelector(".truncate").textContent.trim() + })).reverse(); } - + findChapterTitle(dom) { return dom.querySelector("h2.text-2xl"); } From ee3217b7e2fc565aab38f9750338a7e235e1465d Mon Sep 17 00:00:00 2001 From: The mystery Date: Sat, 7 Nov 2026 11:40:19 +0530 Subject: [PATCH 2/2] Refactor getChapterUrls to simplify chapter retrieval --- plugin/js/parsers/WoopreadParser.js | 42 ++++++----------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/plugin/js/parsers/WoopreadParser.js b/plugin/js/parsers/WoopreadParser.js index 04bf09701..35972249f 100644 --- a/plugin/js/parsers/WoopreadParser.js +++ b/plugin/js/parsers/WoopreadParser.js @@ -25,51 +25,35 @@ class WoopreadParser extends Parser { } async getChapterUrls(dom) { - // The page only renders the newest chapter group in the DOM (older groups are - // collapsed and not rendered), but the full list is embedded in the Next.js - // flight data (self.__next_f.push). Read the full list from there. let chapters = WoopreadParser.chaptersFromFlightData(dom); if (chapters.length === 0) { - // The DOM taken from the open browser tab has already been hydrated: the - // flight data scripts are gone and only the expanded chapter groups exist. - // Re-fetch the raw page, which still contains the full list. - try { - let rawDom = (await HttpClient.wrapFetch(dom.baseURI)).responseXML; - chapters = WoopreadParser.chaptersFromFlightData(rawDom); - } catch (e) { - // fall through to the rendered links - } - } - if (chapters.length === 0) { - return WoopreadParser.chaptersFromRenderedLinks(dom); + // DOM from the open tab is hydrated, so the embedded chapter list is gone + let rawDom = (await HttpClient.wrapFetch(dom.baseURI)).responseXML; + chapters = WoopreadParser.chaptersFromFlightData(rawDom); } - let base = dom.baseURI || "https://woopread.com/"; return chapters .sort((a, b) => a.number - b.number) .map(c => ({ - sourceUrl: new URL(`/series/${c.novelSlug}/${c.slug}`, base).href, + sourceUrl: new URL(`/series/${c.novelSlug}/${c.slug}`, dom.baseURI).href, title: c.title })); } + // Only the newest chapter group is rendered; the full list is in the Next.js flight data static chaptersFromFlightData(dom) { let payload = ""; for (let script of dom.querySelectorAll("script")) { - let text = script.textContent; - if (!text.includes("__next_f.push")) { - continue; - } - let match = text.match(/__next_f\.push\((\[[\s\S]*\])\)\s*;?\s*$/); + let match = script.textContent.match(/__next_f\.push\((\[[\s\S]*\])\)\s*;?\s*$/); if (match === null) { continue; } try { let arr = JSON.parse(match[1]); - if (arr.length > 1 && typeof arr[1] === "string") { + if (typeof arr[1] === "string") { payload += arr[1]; } } catch (e) { - // ignore chunks that are not plain JSON + continue; } } @@ -94,16 +78,6 @@ class WoopreadParser extends Parser { return chapters; } - static chaptersFromRenderedLinks(dom) { - // Fallback: only the expanded chapter group(s) are available this way - let links = [...dom.querySelectorAll("main.grow a[href*='/chapter-'], main.grow a[href*='/side-story-']")] - .filter(a => a.querySelector(".truncate") !== null); - return links.map(a => ({ - sourceUrl: a.href, - title: a.querySelector(".truncate").textContent.trim() - })).reverse(); - } - findChapterTitle(dom) { return dom.querySelector("h2.text-2xl"); }