diff --git a/src/plugins/english/novelupdates.ts b/src/plugins/english/novelupdates.ts
index e44e74331..6aeb553c3 100644
--- a/src/plugins/english/novelupdates.ts
+++ b/src/plugins/english/novelupdates.ts
@@ -6,7 +6,7 @@ import { Plugin } from '@typings/plugin';
class NovelUpdates implements Plugin.PluginBase {
id = 'novelupdates';
name = 'Novel Updates';
- version = '0.9.1';
+ version = '0.10.0';
icon = 'src/en/novelupdates/icon.png';
customCSS = 'src/en/novelupdates/customCSS.css';
site = 'https://www.novelupdates.com/';
@@ -224,20 +224,6 @@ class NovelUpdates implements Plugin.PluginBase {
break;
}
// Last edited in 0.9.0 by Batorian - 19/03/2025
- case 'darkstartranslations': {
- chapterTitle = loadedCheerio('ol.breadcrumb li').last().text().trim();
- chapterContent = loadedCheerio('.text-left').html()!;
- // Load the extracted chapter content into Cheerio
- const chapterCheerio = parseHTML(chapterContent);
- // Add an empty row (extra
) after each
element
- chapterCheerio('br').each((_, el) => {
- chapterCheerio(el).after('
'); // Add one more
for an empty row
- });
- // Get the updated content
- chapterContent = chapterCheerio.html();
- break;
- }
- // Last edited in 0.9.0 by Batorian - 19/03/2025
case 'fictionread': {
bloatElements = [
'.content > style',
@@ -562,6 +548,65 @@ class NovelUpdates implements Plugin.PluginBase {
}
break;
}
+ // Last edited in 0.10.0 by Batorian - 08/07/2025
+ case 'stellarrealm': {
+ // Modular extraction inspired by W2e
+ const extractStellarRealmContent = (cheerioInstance: CheerioAPI) => {
+ // Remove ad-related bloat elements
+ const bloatElements = ['.ad-container', 'script', 'style'];
+ bloatElements.forEach(tag => cheerioInstance(tag).remove());
+
+ // Extract the data-page attribute from
+ const dataPage = cheerioInstance('#app').attr('data-page');
+ if (!dataPage) {
+ throw new Error('data-page attribute not found on Stellar Realm.');
+ }
+
+ // Parse the JSON from data-page
+ let pageData;
+ try {
+ pageData = JSON.parse(dataPage) as {
+ component: string;
+ props: {
+ chapter: {
+ id: number;
+ title: string;
+ content: string;
+ };
+ };
+ };
+ } catch (e) {
+ throw new Error(
+ 'Failed to parse data-page JSON for Stellar Realm.',
+ );
+ }
+
+ let chapterTitle = pageData.props.chapter.title;
+ let chapterContent = pageData.props.chapter.content;
+
+ // Clean up content (remove inline styles/scripts if needed)
+ const chapterCheerio = parseHTML(chapterContent);
+ chapterCheerio('script, style').remove();
+ chapterContent = chapterCheerio.html()!;
+
+ // Return formatted HTML
+ return `
${chapterTitle}
${chapterContent}`;
+ };
+
+ try {
+ chapterText = extractStellarRealmContent(loadedCheerio);
+ } catch (err) {
+ // Fallback: try to extract whatever is in #app or body
+ let fallbackContent =
+ loadedCheerio('#app').html() || loadedCheerio('body').html() || '';
+ // Remove scripts/styles
+ const fallbackCheerio = parseHTML(fallbackContent);
+ fallbackCheerio('script, style').remove();
+ fallbackContent = fallbackCheerio.html()!;
+ chapterText = fallbackContent || 'Unable to extract chapter content.';
+ }
+ break;
+ }
// Last edited in 0.9.0 by Batorian - 19/03/2025
case 'tinytranslation': {
bloatElements = [
@@ -664,239 +709,353 @@ class NovelUpdates implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise {
let chapterText;
+ try {
+ const requestUrl = this.site + chapterPath;
+ console.log('Request URL:', requestUrl);
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
- const url = result.url;
- const domainParts = url.toLowerCase().split('/')[2].split('.');
+ // HEAD fetch to get the final redirect URL
+ let headUrl = requestUrl;
+ try {
+ const headResult = await fetchApi(requestUrl, { method: 'HEAD' });
+ if (headResult && headResult.url) {
+ headUrl = headResult.url;
+ console.log('HEAD redirect URL:', headUrl);
+ }
+ } catch (e) {
+ console.log('HEAD fetch failed, using original URL');
+ }
- const loadedCheerio = parseHTML(body);
+ const result = await fetchApi(headUrl);
+ console.log('Response Status:', result.status);
+ console.log('Final URL:', result.url);
+ if (result.headers && typeof result.headers.forEach === 'function') {
+ const headersObj: Record = {};
+ result.headers.forEach((value, key) => {
+ headersObj[key] = value;
+ });
+ console.log('Response Headers:', headersObj);
+ }
- // Handle CAPTCHA cases
- const blockedTitles = [
- 'bot verification',
- 'just a moment...',
- 'redirecting...',
- 'un instant...',
- 'you are being redirected...',
- ];
- const title = loadedCheerio('title').text().trim().toLowerCase();
- if (blockedTitles.includes(title)) {
- throw new Error('Captcha detected, please open in webview.');
- }
+ const body = await result.text();
+ console.log('Response Body (first 500 chars):', body.substring(0, 500));
+ const url = result.url;
+ const domainParts = url.toLowerCase().split('/')[2].split('.');
- // Check if chapter url is wrong or site is down
- if (!result.ok) {
- throw new Error(
- `Failed to fetch ${result.url}: ${result.status} ${result.statusText}`,
- );
- }
+ const loadedCheerio = parseHTML(body);
- // Detect platforms
- let isBlogspot = ['blogspot', 'blogger'].some(keyword =>
- [
- loadedCheerio('meta[name="google-adsense-platform-domain"]').attr(
- 'content',
- ),
- loadedCheerio('meta[name="generator"]').attr('content'),
- ].some(meta => meta?.toLowerCase().includes(keyword)),
- );
+ // Handle CAPTCHA cases
+ const blockedTitles = [
+ 'bot verification',
+ 'just a moment...',
+ 'redirecting...',
+ 'un instant...',
+ 'you are being redirected...',
+ ];
+ const title = loadedCheerio('title').text().trim().toLowerCase();
+ console.log('Page Title:', title);
+ if (blockedTitles.includes(title)) {
+ console.log('Falling back to webview for URL:', requestUrl);
+ throw new Error('Captcha detected, please open in webview.');
+ }
- let isWordPress = ['wordpress', 'site kit by google'].some(keyword =>
- [
- loadedCheerio('#dcl_comments-js-extra').html(),
- loadedCheerio('meta[name="generator"]').attr('content'),
- loadedCheerio('.powered-by').text(),
- loadedCheerio('footer').text(),
- ].some(meta => meta?.toLowerCase().includes(keyword)),
- );
+ // Check if chapter url is wrong or site is down
+ if (!result.ok) {
+ console.log('Fetch failed:', result.status, result.statusText);
+ throw new Error(
+ `Failed to fetch ${result.url}: ${result.status} ${result.statusText}`,
+ );
+ }
- // Manually set WordPress flag for known sites
- const manualWordPress = ['etherreads', 'greenztl2', 'soafp'];
- if (!isWordPress && domainParts.some(wp => manualWordPress.includes(wp))) {
- isWordPress = true;
- }
+ // Detect platforms
+ let isBlogspot = false;
+ const blogspotEvidence: string[] = [];
+ const blogspotChecks = [
+ {
+ label: 'meta[name="google-adsense-platform-domain"]',
+ value: loadedCheerio(
+ 'meta[name="google-adsense-platform-domain"]',
+ ).attr('content'),
+ },
+ {
+ label: 'meta[name="generator"]',
+ value: loadedCheerio('meta[name="generator"]').attr('content'),
+ },
+ { label: 'body class', value: loadedCheerio('body').attr('class') },
+ {
+ label: 'blogspot in html',
+ value: loadedCheerio.html().includes('blogspot')
+ ? 'blogspot found'
+ : '',
+ },
+ {
+ label: 'blogger in html',
+ value: loadedCheerio.html().includes('blogger')
+ ? 'blogger found'
+ : '',
+ },
+ ];
+ for (const check of blogspotChecks) {
+ if (
+ check.value &&
+ typeof check.value === 'string' &&
+ (check.value.toLowerCase().includes('blogspot') ||
+ check.value.toLowerCase().includes('blogger'))
+ ) {
+ isBlogspot = true;
+ blogspotEvidence.push(`${check.label}: ${check.value}`);
+ }
+ }
+ if (isBlogspot) {
+ console.log('Blogspot detected! Evidence:', blogspotEvidence);
+ } else {
+ console.log(
+ 'Blogspot NOT detected. Evidence checked:',
+ blogspotChecks.map(c => `${c.label}: ${c.value}`),
+ );
+ }
- // Handle outlier sites
- const outliers = [
- 'anotivereads',
- 'arcanetranslations',
- 'asuratls',
- 'darkstartranslations',
- 'fictionread',
- 'helscans',
- 'infinitenoveltranslations',
- 'mirilu',
- 'novelworldtranslations',
- 'sacredtexttranslations',
- 'stabbingwithasyringe',
- 'tinytranslation',
- 'vampiramtl',
- 'zetrotranslation',
- ];
- if (domainParts.some(d => outliers.includes(d))) {
- isWordPress = false;
- isBlogspot = false;
- }
+ // Improved WordPress detection with debug logs
+ let isWordPress = false;
+ const wpEvidence: string[] = [];
+ const wpChecks = [
+ {
+ label: 'meta[name="generator"]',
+ value: loadedCheerio('meta[name="generator"]').attr('content'),
+ },
+ {
+ label: '#dcl_comments-js-extra',
+ value: loadedCheerio('#dcl_comments-js-extra').html(),
+ },
+ { label: '.powered-by', value: loadedCheerio('.powered-by').text() },
+ { label: 'footer', value: loadedCheerio('footer').text() },
+ { label: 'body class', value: loadedCheerio('body').attr('class') },
+ {
+ label: 'wp-content in html',
+ value: loadedCheerio.html().includes('wp-content')
+ ? 'wp-content found'
+ : '',
+ },
+ {
+ label: 'window._wpemojiSettings',
+ value: loadedCheerio.html().includes('_wpemojiSettings')
+ ? '_wpemojiSettings found'
+ : '',
+ },
+ ];
+ for (const check of wpChecks) {
+ if (
+ check.value &&
+ typeof check.value === 'string' &&
+ (check.value.toLowerCase().includes('wordpress') ||
+ check.value.toLowerCase().includes('site kit by google') ||
+ check.value.toLowerCase().includes('wp-content') ||
+ check.value.toLowerCase().includes('wpemoji'))
+ ) {
+ isWordPress = true;
+ wpEvidence.push(`${check.label}: ${check.value}`);
+ }
+ }
+ if (isWordPress) {
+ console.log('WordPress detected! Evidence:', wpEvidence);
+ } else {
+ console.log(
+ 'WordPress NOT detected. Evidence checked:',
+ wpChecks.map(c => `${c.label}: ${c.value}`),
+ );
+ }
+
+ // Handle outlier sites
+ const outliers = [
+ 'anotivereads',
+ 'arcanetranslations',
+ 'asuratls',
+ 'fictionread',
+ 'helscans',
+ 'infinitenoveltranslations',
+ 'mirilu',
+ 'novelworldtranslations',
+ 'sacredtexttranslations',
+ 'stabbingwithasyringe',
+ 'tinytranslation',
+ 'vampiramtl',
+ 'zetrotranslation',
+ ];
+ if (domainParts.some(d => outliers.includes(d))) {
+ isWordPress = false;
+ isBlogspot = false;
+ }
- // Last edited in 0.9.0 - 19/03/2025
- /**
- * Blogspot sites:
- * - ¼-Assed
- * - AsuraTls (Outlier)
- * - FictionRead (Outlier)
- * - Novel World Translations (Outlier)
- * - SacredText TL (Outlier)
- * - Toasteful
- *
- * WordPress sites:
- * - Anomlaously Creative (Outlier)
- * - Arcane Translations (Outlier)
- * - Blossom Translation
- * - Darkstar Translations (Outlier)
- * - Dumahs Translations
- * - ElloMTL
- * - Femme Fables
- * - Gadgetized Panda Translation
- * - Gem Novels
- * - Goblinslate
- * - GreenzTL
- * - Hel Scans (Outlier)
- * - ippotranslations
- * - JATranslations
- * - Light Novels Translations
- * - Mirilu - Novel Reader Attempts Translating (Outlier)
- * - Neosekai Translations
- * - Shanghai Fantasy
- * - Soafp (Manually added)
- * - Stabbing with a Syringe (Outlier)
- * - StoneScape
- * - TinyTL (Outlier)
- * - VampiraMTL (Outlier)
- * - Wonder Novels
- * - Yong Library
- * - Zetro Translation (Outlier)
- */
+ // Last edited in 0.10.0 - 08/07/2025
+ /**
+ * Blogspot sites:
+ * - ¼-Assed
+ * - AsuraTls (Outlier)
+ * - FictionRead (Outlier)
+ * - Novel World Translations (Outlier)
+ * - SacredText TL (Outlier)
+ * - Toasteful
+ *
+ * WordPress sites:
+ * - Anomlaously Creative (Outlier)
+ * - Arcane Translations (Outlier)
+ * - Blossom Translation
+ * - Dumah's Translations
+ * - ElloMTL
+ * - Ether Reads
+ * - Femme Fables
+ * - Gadgetized Panda Translation
+ * - Gem Novels
+ * - Goblinslate
+ * - GreenzTL (Taken down)
+ * - Hel Scans (Outlier)
+ * - Hiraeth Translation
+ * - ippotranslations
+ * - JATranslations
+ * - Light Novels Translations
+ * - Mirilu - Novel Reader Attempts Translating (Outlier)
+ * - Neosekai Translations
+ * - Noice Translations
+ * - Shanghai Fantasy
+ * - Soafp
+ * - Stabbing with a Syringe (Outlier)
+ * - StoneScape
+ * - TinyTL (Outlier)
+ * - VampiraMTL (Outlier)
+ * - Wonder Novels
+ * - Yong Library
+ * - Zetro Translation (Outlier)
+ */
- // Fetch chapter content based on detected platform
- if (!isWordPress && !isBlogspot) {
- chapterText = await this.getChapterBody(loadedCheerio, domainParts, url);
- } else {
- const bloatElements = isBlogspot
- ? ['.button-container', '.ChapterNav', '.ch-bottom', '.separator']
- : [
- '.ad',
- '.author-avatar',
- '.chapter-warning',
- '.entry-meta',
- '.ezoic-ad',
- '.mb-center',
- '.modern-footnotes-footnote__note',
- '.patreon-widget',
- '.post-cats',
- '.pre-bar',
- '.sharedaddy',
- '.sidebar',
- '.swg-button-v2-light',
- '.wp-block-buttons',
- //'.wp-block-columns',
- '.wp-dark-mode-switcher',
- '.wp-next-post-navi',
- '#hpk',
- '#jp-post-flair',
- '#textbox',
- ];
+ // Fetch chapter content based on detected platform
+ if (!isWordPress && !isBlogspot) {
+ chapterText = await this.getChapterBody(
+ loadedCheerio,
+ domainParts,
+ url,
+ );
+ } else {
+ const bloatElements = isBlogspot
+ ? ['.button-container', '.ChapterNav', '.ch-bottom', '.separator']
+ : [
+ '.ad',
+ '.author-avatar',
+ '.chapter-warning',
+ '.entry-meta',
+ '.ezoic-ad',
+ '.mb-center',
+ '.modern-footnotes-footnote__note',
+ '.patreon-widget',
+ '.post-cats',
+ '.pre-bar',
+ '.sharedaddy',
+ '.sidebar',
+ '.swg-button-v2-light',
+ '.wp-block-buttons',
+ //'.wp-block-columns',
+ '.wp-dark-mode-switcher',
+ '.wp-next-post-navi',
+ '#hpk',
+ '#jp-post-flair',
+ '#textbox',
+ ];
- bloatElements.forEach(tag => loadedCheerio(tag).remove());
+ bloatElements.forEach(tag => loadedCheerio(tag).remove());
- // Extract title
- const titleSelectors = isBlogspot
- ? ['.entry-title', '.post-title', 'head title']
- : [
- '.entry-title',
- '.chapter__title',
- '.title-content',
- '.wp-block-post-title',
- '.title_story',
- '#chapter-heading',
- 'head title',
- 'h1:first-of-type',
- 'h2:first-of-type',
- '.active',
- ];
- let chapterTitle = titleSelectors
- .map(sel => loadedCheerio(sel).first().text())
- .find(text => text);
+ // Extract title
+ const titleSelectors = isBlogspot
+ ? ['.entry-title', '.post-title', 'head title']
+ : [
+ 'li.active',
+ '.entry-title',
+ '.chapter__title',
+ '.title-content',
+ '.wp-block-post-title',
+ '.title_story',
+ '#chapter-heading',
+ 'head title',
+ 'h1:first-of-type',
+ 'h2:first-of-type',
+ '.active',
+ ];
+ let chapterTitle = titleSelectors
+ .map(sel => loadedCheerio(sel).first().text())
+ .find(text => text);
- // Extract subtitle (if any)
- const chapterSubtitle =
- loadedCheerio('.cat-series').first().text() ||
- loadedCheerio('h1.leading-none ~ span').first().text();
- if (chapterSubtitle) chapterTitle = chapterSubtitle;
+ // Extract subtitle (if any)
+ const chapterSubtitle =
+ loadedCheerio('.cat-series').first().text() ||
+ loadedCheerio('h1.leading-none ~ span').first().text();
+ if (chapterSubtitle) chapterTitle = chapterSubtitle;
- // Extract content
- const contentSelectors = isBlogspot
- ? ['.content-post', '.entry-content', '.post-body']
- : [
- '.chapter__content',
- '.entry-content',
- '.text_story',
- '.post-content',
- '.contenta',
- '.single_post',
- '.main-content',
- '.reader-content',
- '#content',
- '#the-content',
- 'article.post',
- ];
- const chapterContent = contentSelectors
- .map(sel => loadedCheerio(sel).html()!)
- .find(html => html);
+ // Extract content
+ const contentSelectors = isBlogspot
+ ? ['.content-post', '.entry-content', '.post-body']
+ : [
+ '.chapter__content',
+ '.entry-content',
+ '.text_story',
+ '.post-content',
+ '.contenta',
+ '.single_post',
+ '.main-content',
+ '.reader-content',
+ '#content',
+ '#the-content',
+ 'article.post',
+ ];
+ const chapterContent = contentSelectors
+ .map(sel => loadedCheerio(sel).html()!)
+ .find(html => html);
- if (chapterTitle) {
- chapterText = `${chapterTitle}
${chapterContent}`;
- } else {
- chapterText = chapterContent;
+ if (chapterTitle) {
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ } else {
+ chapterText = chapterContent;
+ }
}
- }
- // Fallback content extraction
- if (!chapterText) {
- ['nav', 'header', 'footer', '.hidden'].forEach(tag =>
- loadedCheerio(tag).remove(),
- );
- chapterText = loadedCheerio('body').html()!;
- }
+ // Fallback content extraction
+ if (!chapterText) {
+ console.log('Fallback: extracting from body');
+ ['nav', 'header', 'footer', '.hidden'].forEach(tag =>
+ loadedCheerio(tag).remove(),
+ );
+ chapterText = loadedCheerio('body').html()!;
+ }
- // Convert relative URLs to absolute
- chapterText = chapterText.replace(
- /href="\//g,
- `href="${this.getLocation(result.url)}/`,
- );
+ // Convert relative URLs to absolute
+ chapterText = chapterText.replace(
+ /href="\//g,
+ `href="${this.getLocation(result.url)}/`,
+ );
- // Process images
- const chapterCheerio = parseHTML(chapterText);
- chapterCheerio('noscript').remove();
+ // Process images
+ const chapterCheerio = parseHTML(chapterText);
+ chapterCheerio('noscript').remove();
- chapterCheerio('img').each((_, el) => {
- const $el = chapterCheerio(el);
+ chapterCheerio('img').each((_, el) => {
+ const $el = chapterCheerio(el);
- // Only update if the lazy-loaded attribute exists
- if ($el.attr('data-lazy-src')) {
- $el.attr('src', $el.attr('data-lazy-src'));
- }
- if ($el.attr('data-lazy-srcset')) {
- $el.attr('srcset', $el.attr('data-lazy-srcset'));
- }
+ // Only update if the lazy-loaded attribute exists
+ if ($el.attr('data-lazy-src')) {
+ $el.attr('src', $el.attr('data-lazy-src'));
+ }
+ if ($el.attr('data-lazy-srcset')) {
+ $el.attr('srcset', $el.attr('data-lazy-srcset'));
+ }
- // Remove lazy-loading class if it exists
- if ($el.hasClass('lazyloaded')) {
- $el.removeClass('lazyloaded');
- }
- });
+ // Remove lazy-loading class if it exists
+ if ($el.hasClass('lazyloaded')) {
+ $el.removeClass('lazyloaded');
+ }
+ });
- return chapterCheerio.html()!;
+ console.log('Returning chapter HTML');
+ return chapterCheerio.html()!;
+ } catch (error) {
+ console.error('Fetch Error:', error);
+ throw new Error(`Network request failed: ${error}`);
+ }
}
async searchNovels(