Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 37 additions & 42 deletions plugins/arabic/galaxynovels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/';

Expand Down Expand Up @@ -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<string> {
const res = await fetchApi(url);
if (!res.ok) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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] || '',
}));
Expand All @@ -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-<post-id>/
// answers 404.
const path = this.toChapterPath(chapterLink);
const numMatch = path.match(/chapter-(\d+)/);
const chapterNumber = numMatch ? parseInt(numMatch[1]) : 0;

Expand All @@ -210,27 +209,27 @@ class GalaxyNovels implements Plugin.PluginBase {
}

async parseChapter(chapterPath: string): Promise<string> {
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<ChapterContentResponse>(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 || '<p>Content not available.</p>';
return '<p>Content not available.</p>';
}

async searchNovels(
Expand All @@ -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;
Expand All @@ -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}`,
}));
}
}
Expand Down
27 changes: 21 additions & 6 deletions plugins/multisrc/lightnovelwp/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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 <article> instead of a <div> and no longer close with
// `<div class="bottomnav">`. 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(/<div.*?class="epcontent ([^]*?)<div.*?class="?bottomnav/g)?.[0]
.match(/<p[^>]*>([^]*?)<\/p>/g)
?.join('\n') || ''
$('.epcontent')
.first()
.find('p')
.map((_, el) => $.html(el) || '')
.get()
.join('\n') || ''
);
}

Expand Down
2 changes: 1 addition & 1 deletion plugins/multisrc/madara/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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();"
}
},
{
Expand Down
69 changes: 63 additions & 6 deletions plugins/multisrc/madara/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -189,13 +189,17 @@ export class MadaraPlugin implements Plugin.PluginBase {
loadedCheerio('.post-title h1').text().trim() ||
loadedCheerio('#manga-title h1').text().trim() ||
loadedCheerio('.manga-title').text().trim() ||
loadedCheerio('h1.nhv-novel-title').text().trim() ||
'',
};

novel.cover =
loadedCheerio('.summary_image > 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 () {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -385,11 +424,29 @@ export class MadaraPlugin implements Plugin.PluginBase {

async parseChapter(chapterPath: string): Promise<string> {
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 {
Expand Down
Loading