diff --git a/plugins/arabic/galaxynovels.ts b/plugins/arabic/galaxynovels.ts index 155c1dae7..810895fda 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,29 @@ 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.

'; + // Throw rather than return placeholder text, which the reader would show + // (and a download would save) as if it were the chapter. + throw new Error('Chapter text not found on the page, open it in WebView.'); } async searchNovels( @@ -254,9 +255,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 +264,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}`, })); } } diff --git a/plugins/multisrc/lightnovelwp/template.ts b/plugins/multisrc/lightnovelwp/template.ts index ce450b846..1769bdffa 100644 --- a/plugins/multisrc/lightnovelwp/template.ts +++ b/plugins/multisrc/lightnovelwp/template.ts @@ -42,7 +42,7 @@ export class LightNovelWPPlugin implements Plugin.PluginBase { this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `1.1.${11 + versionIncrements}`; + this.version = `1.2.${11 + versionIncrements}`; this.options = metadata.options ?? ({} as LightNovelWPOptions); this.filters = metadata.filters satisfies Filters; @@ -220,7 +220,13 @@ export class LightNovelWPPlugin implements Plugin.PluginBase { isReadingChapter = true; } else if (isReadingChapter) { if (name === 'a' && tempChapter.path === undefined) { - tempChapter.path = attribs['href'].replace(baseURL, '').trim(); + // Some installs link chapters with the absolute URL of the main + // site (free.kolnovel.com links kolnovel.com), and parseChapter + // re-prefixes this.site, so keep the path only. + tempChapter.path = attribs['href'] + .replace(baseURL, '') + .replace(/^https?:\/\/[^/]+/, '') + .trim(); } else if (attribs['class'] === 'epl-num') { isReadingChapterInfo = 1; } else if (attribs['class'] === 'epl-title') { @@ -438,17 +444,21 @@ export class LightNovelWPPlugin implements Plugin.PluginBase { throw error; } } - // The chapter lives in the `epcontent` block. Older child themes closed - // it with a `bottomnav` div, but that wrapper is gone on current ones, so - // select the element itself rather than cutting the document between - // landmarks: a