From 03ed00663ceb0fbedd6390fa27df807771553770 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Wed, 16 Sep 2026 20:11:56 +0800 Subject: [PATCH 1/5] fix(ar/galaxynovels): read chapter text from the served chapter page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseNovel preferred the data-chapter-id attribute (a WordPress post id) over the chapter link and built `…/chapter-/` paths, which the site answers with 404 — only `…/chapter-//` resolves, so every chapter list it produced was unusable. parseChapter then asked `wp-json/wor-reader-app/v1/chapters/` first; that route exists but Cloudflare answers 403 to every non-app client (reproduced from a real browser with a same-origin fetch), and the HTML fallback looked for selectors the theme no longer ships. Read the body from the article marked up as https://schema.org/Chapter instead, taking the first candidate that actually contains text. Fixes the chapter body for Galaxy Novels (issue #2522). --- plugins/arabic/galaxynovels.ts | 79 ++++++++++++++++------------------ 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/plugins/arabic/galaxynovels.ts b/plugins/arabic/galaxynovels.ts index 155c1dae7..9be591784 100644 --- a/plugins/arabic/galaxynovels.ts +++ b/plugins/arabic/galaxynovels.ts @@ -21,19 +21,10 @@ type ChaptersIndex = { chapters: ChapterJSON[]; }; -type ChapterContentResponse = { - schema: number; - data: { - content_html: string; - display_title: string; - navigation: { next_url: string; previous_url: string }; - }; -}; - class GalaxyNovels implements Plugin.PluginBase { id = 'galaxynovels'; name = 'Galaxy Novels'; - version = '1.1.0'; + version = '1.1.1'; icon = 'src/ar/galaxynovels/icon.png'; site = 'https://galaxynovels.com/'; @@ -62,6 +53,11 @@ class GalaxyNovels implements Plugin.PluginBase { private baseUrl = 'https://galaxynovels.com'; + private toChapterPath(url: string | undefined): string { + if (!url) return ''; + return url.startsWith('http') ? new URL(url).pathname : url; + } + private async fetchHtml(url: string): Promise { const res = await fetchApi(url); if (!res.ok) { @@ -106,8 +102,7 @@ class GalaxyNovels implements Plugin.PluginBase { const coverLink = $el.find('a.wor-novel-card__cover'); const href = coverLink.attr('href'); const img = $el.find('img.wor-cover-img'); - const cover = - img.attr('data-src') || img.attr('src') || undefined; + const cover = img.attr('data-src') || img.attr('src') || undefined; const title = $el.find('h3 a').text().trim(); if (!href || !title) return; @@ -162,7 +157,7 @@ class GalaxyNovels implements Plugin.PluginBase { chapters = index.chapters.map(ch => ({ name: ch.label + (ch.title ? `: ${ch.title}` : ''), - path: `${novelPath}chapter-${ch.id}/`, + path: this.toChapterPath(ch.url) || `${novelPath}chapter-${ch.id}/`, chapterNumber: ch.position, releaseTime: ch.date_iso?.split('T')[0] || '', })); @@ -174,17 +169,21 @@ class GalaxyNovels implements Plugin.PluginBase { if (chapters.length === 0) { $('article.wor-novel-chapter-item').each((_, el) => { const $el = $(el); - const chapterLink = $el.find('h3 a').attr('href') || $el.find('a.wor-novel-chapter-item__num').attr('href'); - const chapterName = $el.find('h3 a').text().trim() || $el.find('a.wor-novel-chapter-item__num').text().trim(); - const chapterId = $el.attr('data-chapter-id'); + const chapterLink = + $el.find('h3 a').attr('href') || + $el.find('a.wor-novel-chapter-item__num').attr('href'); + const chapterName = + $el.find('h3 a').text().trim() || + $el.find('a.wor-novel-chapter-item__num').text().trim(); const timeEl = $el.find('time'); const releaseTime = timeEl.attr('datetime')?.split('T')[0] || ''; if (!chapterLink) return; - const path = chapterId - ? `${novelPath}chapter-${chapterId}/` - : new URL(chapterLink, this.site).pathname; + // Only the full permalink resolves on the site; the data-chapter-id + // attribute holds the WordPress post id, and …/chapter-/ + // answers 404. + const path = this.toChapterPath(chapterLink); const numMatch = path.match(/chapter-(\d+)/); const chapterNumber = numMatch ? parseInt(numMatch[1]) : 0; @@ -210,27 +209,27 @@ class GalaxyNovels implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const idMatch = chapterPath.match(/chapter-(\d+)/); - const chapterId = idMatch?.[1]; + const url = `${this.baseUrl}${chapterPath}`; + const html = await this.fetchHtml(url); + const $ = loadCheerio(html); - if (chapterId) { - const apiUrl = `${this.baseUrl}/wp-json/wor-reader-app/v1/chapters/${chapterId}`; - try { - const response = await this.fetchJson(apiUrl); - if (response.data?.content_html) { - return response.data.content_html; - } - } catch { - // fallback to HTML + // The body is server-rendered inside the article marked up as + // https://schema.org/Chapter, as the element with itemprop="text". Pick + // the first candidate that actually carries text: a themed wrapper can be + // present but empty, and cheerio selections are always truthy. + for (const selector of [ + 'article[itemtype*="Chapter"] [itemprop="text"]', + '[itemprop="text"]', + '[data-wor-reader-text]', + '.wor-reader-text-surface', + ]) { + const candidate = $(selector); + if (candidate.length && candidate.text().trim()) { + return candidate.html() || ''; } } - const url = `${this.baseUrl}${chapterPath}`; - const html = await this.fetchHtml(url); - const $ = loadCheerio(html); - const content = - $('article.wor-chapter-content, .wor-chapter-text, .entry-content').html(); - return content || '

Content not available.

'; + return '

Content not available.

'; } async searchNovels( @@ -254,9 +253,7 @@ class GalaxyNovels implements Plugin.PluginBase { const term = searchTerm.toLowerCase(); const filtered = searchIndex.items.filter( - n => - n.t.toLowerCase().includes(term) || - n.s.toLowerCase().includes(term), + n => n.t.toLowerCase().includes(term) || n.s.toLowerCase().includes(term), ); const limit = 20; @@ -265,9 +262,7 @@ class GalaxyNovels implements Plugin.PluginBase { return filtered.slice(offset, offset + limit).map(novel => ({ name: novel.t, path: novel.u, - cover: novel.c.startsWith('http') - ? novel.c - : `${this.baseUrl}${novel.c}`, + cover: novel.c.startsWith('http') ? novel.c : `${this.baseUrl}${novel.c}`, })); } } From 34e267cc5c6d358dbfdf922a80d47b8d5c872be9 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Wed, 16 Sep 2026 20:12:06 +0800 Subject: [PATCH 2/5] fix(madara): keep the first chapter body that has text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `$('.text-left') || $('.text-right') || …` never reaches the second selector: a cheerio selection object is always truthy, so a page without `.text-left` returned an empty chapter instead of trying the rest. Walk the selectors and keep the first one that actually contains text, and add the anchors the Madara-derived themes read from (`.text-content`, `.text-chapter-content`, `novel-chapter`, `.reading-content`). The riwyat source (cenele.com) also hides scraped-text decoys in `
` blocks that its inline CSS makes invisible and wraps the body in a per-request random class, so its customJs now drops those sections along with in-body style/script tags and the app-promo blocks. Fixes the chapter body for Riwyat (issue #2522). --- plugins/multisrc/madara/sources.json | 2 +- plugins/multisrc/madara/template.ts | 30 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/plugins/multisrc/madara/sources.json b/plugins/multisrc/madara/sources.json index e4413a8a1..a8cd01a17 100644 --- a/plugins/multisrc/madara/sources.json +++ b/plugins/multisrc/madara/sources.json @@ -137,7 +137,7 @@ "options": { "useNewChapterEndpoint": true, "lang": "Arabic", - "customJs": "chapterText.find('span[style*=\"opacity: 0; position: fixed;\"],[role=\"presentation\"]').remove();" + "customJs": "chapterText.find('style,script,[inert],[data-nosnippet],[data-nhv-reader-promo]').remove();chapterText.find('span[style*=\"opacity: 0; position: fixed;\"],[role=\"presentation\"]').remove();" } }, { diff --git a/plugins/multisrc/madara/template.ts b/plugins/multisrc/madara/template.ts index 91b01be6c..32416a5d2 100644 --- a/plugins/multisrc/madara/template.ts +++ b/plugins/multisrc/madara/template.ts @@ -46,7 +46,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.2.${versionIncrements}`; + this.version = `2.3.${versionIncrements}`; this.options = metadata.options; this.filters = metadata.filters; @@ -385,11 +385,29 @@ export class MadaraPlugin implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { const loadedCheerio = await this.getCheerio(this.site + chapterPath, false); - const chapterText = - loadedCheerio('.text-left') || - loadedCheerio('.text-right') || - loadedCheerio('.entry-content') || - loadedCheerio('.c-blog-post > div > div:nth-child(2)'); + + // A cheerio selection is always truthy, so the previous + // `$('.text-left') || $('.text-right') || …` chain never looked past the + // first selector: on a site that no longer renders `.text-left` it + // returned an empty body no matter what else the page offered. Walk the + // candidates and keep the first one that actually holds text. + let chapterText = loadedCheerio('.__no-chapter-content__'); + for (const selector of [ + '.text-left', + '.text-right', + '.text-content', + '.text-chapter-content', + 'novel-chapter', + '.reading-content', + '.entry-content', + '.c-blog-post > div > div:nth-child(2)', + ]) { + const candidate = loadedCheerio(selector); + if (candidate.text().trim()) { + chapterText = candidate; + break; + } + } if (this.options?.customJs) { try { From cfa2b56c6f485f95ee0a4c9092a4b1b0e8295a68 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Wed, 16 Sep 2026 20:12:11 +0800 Subject: [PATCH 3/5] fix(lightnovelwp): read the episode body instead of slicing the page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chapter body regex required a `
` wrapper and a `
` terminator, and sliced the raw document between them. Newer installs render the body as `
` and close it with `