From d6c59019d07f71444a033c771bfbc951e72603b2 Mon Sep 17 00:00:00 2001 From: Fake Name Date: Mon, 29 Jun 2026 20:20:08 -0700 Subject: [PATCH 1/8] Lots of patching for FreeWebNovel's recent changes. --- plugin/js/parsers/FreeWebNovelParser.js | 137 ++++++++++++++++++++++- unitTest/Tests.html | 1 + unitTest/UtestFreeWebNovelParser.js | 138 ++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 unitTest/UtestFreeWebNovelParser.js diff --git a/plugin/js/parsers/FreeWebNovelParser.js b/plugin/js/parsers/FreeWebNovelParser.js index 4bfaeded4..7d3c60776 100644 --- a/plugin/js/parsers/FreeWebNovelParser.js +++ b/plugin/js/parsers/FreeWebNovelParser.js @@ -16,9 +16,55 @@ class FreeWebNovelParser extends Parser { this.minimumThrottle = 1000; } - async getChapterUrls(dom) { + async getChapterUrls(dom, chapterUrlsUI) { let menu = dom.querySelector("ul#idData"); - return util.hyperlinksToChapterList(menu); + let chapters = util.hyperlinksToChapterList(menu); + + let totalPage = 1; + let indexSelect = dom.querySelector("#indexselect"); + if (indexSelect) { + totalPage = indexSelect.querySelectorAll("option").length; + } else { + let scripts = [...dom.querySelectorAll("script")]; + for (let script of scripts) { + let match = /totalPage:\s*(\d+)/.exec(script.textContent); + if (match) { + totalPage = parseInt(match[1]); + break; + } + } + } + + if (totalPage > 1) { + chapterUrlsUI.showTocProgress(chapters); + let baseUrl = dom.baseURI; + let urlObj = new URL(baseUrl); + urlObj.search = ""; + urlObj.hash = ""; + let baseNovelUrl = urlObj.toString(); + + for (let page = 2; page <= totalPage; ++page) { + await this.rateLimitDelay(); + let url = `${baseNovelUrl}?ajax=chapters&page=${page}`; + try { + let response = await HttpClient.fetchJson(url); + if (response && response.json && response.json.code === 200 && response.json.html) { + let parser = new DOMParser(); + let tempDom = parser.parseFromString(response.json.html, "text/html"); + util.setBaseTag(url, tempDom); + let partialChapters = util.hyperlinksToChapterList(tempDom); + if (partialChapters.length > 0) { + chapterUrlsUI.showTocProgress(partialChapters); + chapters = chapters.concat(partialChapters); + } + } + } catch (e) { + console.error("Failed to fetch TOC page: " + page, e); + } + } + } + + return chapters; } extractTitleImpl(dom) { @@ -26,11 +72,16 @@ class FreeWebNovelParser extends Parser { } extractAuthor(dom) { - return dom.querySelector("[title=Author]").parentNode.querySelector("a").textContent; + let element = dom.querySelector("[title=Author]"); + return element ? element.parentNode.querySelector("a").textContent.trim() : ""; } extractSubject(dom) { - let tags = [...dom.querySelector("[title=Genre]").parentNode.querySelectorAll("a")]; + let element = dom.querySelector("[title=Genre]"); + if (!element) { + return ""; + } + let tags = [...element.parentNode.querySelectorAll("a")]; return tags.map(e => e.textContent.trim()).join(", "); } @@ -43,12 +94,78 @@ class FreeWebNovelParser extends Parser { } findContent(dom) { - return dom.querySelector("div.txt"); + return dom.querySelector("div#article") || dom.querySelector("div.txt"); } getInformationEpubItemChildNodes(dom) { return [...dom.querySelectorAll("div.inner")]; } + + removeUnwantedElementsFromContentElement(content) { + // Remove ads injected by third-party ad networks (such as SSP ads and PubFuture networks) + // whose div IDs start with 'bg-ssp-' or 'pf-' + util.removeChildElementsMatchingSelector(content, "div[id^='bg-ssp-']"); + util.removeChildElementsMatchingSelector(content, "div[id^='pf-']"); + + // Clean up any remaining ad divs or empty wrapper divs left behind after ads are deleted + for (let div of content.querySelectorAll("div")) { + if (div.id.startsWith("bg-ssp-") || div.id.startsWith("pf-")) { + div.remove(); + } + // Remove parent wrapper divs if they are now completely empty + if (div.children.length === 0 && div.textContent.trim() === "") { + div.remove(); + } + } + + // Convert escaped/literal HTML tags (like <strong> or <b>) in text nodes to actual DOM elements + let walker = content.ownerDocument.createTreeWalker( + content, + NodeFilter.SHOW_TEXT, + null, + false + ); + let nodesToReplace = []; + let node; + while ((node = walker.nextNode())) { + let val = node.nodeValue; + if (val && /( +