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}`, })); } } diff --git a/plugins/multisrc/lightnovelwp/template.ts b/plugins/multisrc/lightnovelwp/template.ts index 8234b5cf4..25396fdf6 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.${10 + versionIncrements}`; + this.version = `1.2.${10 + 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,11 +444,20 @@ export class LightNovelWPPlugin implements Plugin.PluginBase { throw error; } } + + // The body is the theme's `.epcontent` block, which newer installs render + // as an
instead of a
and no longer close with + // `
`. The old regex sliced the raw document between + // those two wrappers, so both of those changes emptied every chapter; + // select the body itself and read its paragraphs instead. + const $ = load(data); return ( - data - .match(/ a > img').attr('data-lazy-src') || loadedCheerio('.summary_image > a > img').attr('data-src') || loadedCheerio('.summary_image > a > img').attr('src') || + loadedCheerio('.nhv-novel-cover img').attr('data-lazy-src') || + loadedCheerio('.nhv-novel-cover img').attr('data-src') || + loadedCheerio('.nhv-novel-cover img').attr('src') || defaultCover; loadedCheerio('.post-content_item, .post-content').each(function () { @@ -251,6 +255,36 @@ export class MadaraPlugin implements Plugin.PluginBase { } }); + // Fallback for the custom "NHV" theme (e.g. cenele.com), which renders + // novel pages without the classic Madara detail blocks. Every field is + // only filled when the selectors above left it empty, so sources on the + // classic theme keep their existing behaviour. + { + if (!novel.author) + novel.author = + loadedCheerio('.nhv-novel-meta a[href*="cont-author"]') + .first() + .text() + .trim() || + loadedCheerio('a[href*="cont-author"]').first().text().trim(); + if (!novel.genres) + novel.genres = loadedCheerio('.nhv-novel-genres a') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join(', '); + const nhvStatus = loadedCheerio('.nhv-novel-status').text().trim(); + if (!novel.status && nhvStatus) + novel.status = nhvStatus.includes('مستمرة') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + if (!novel.rating) { + const nhvRating = parseFloat( + loadedCheerio('.nhv-simple-rating__avg').text().trim(), + ); + + if (!isNaN(nhvRating)) novel.rating = nhvRating; + } + } // Checks for "Madara NovelHub" version { if (!novel.genres) @@ -292,6 +326,11 @@ export class MadaraPlugin implements Plugin.PluginBase { .join('\n\n') .trim() || loadedCheerio('.manga-excerpt p') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join('\n\n') + .trim() || + loadedCheerio('.nhv-novel-synopsis p') .map((i, el) => loadedCheerio(el).text()) .get() .join('\n\n') @@ -385,11 +424,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 {