Skip to content
Merged
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
191 changes: 152 additions & 39 deletions src/plugins/english/novelupdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.9.2';
icon = 'src/en/novelupdates/icon.png';
customCSS = 'src/en/novelupdates/customCSS.css';
site = 'https://www.novelupdates.com/';
Expand Down Expand Up @@ -72,16 +72,16 @@ class NovelUpdates implements Plugin.PluginBase {

url += '&pg=' + page;

const result = await fetchApi(url);
const body = await result.text();
const response = await fetchApi(url);
const body = await response.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}

async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const url = this.site + novelPath;
const result = await fetchApi(url);
const body = await result.text();
const response = await fetchApi(url);
const body = await response.text();
const loadedCheerio = parseHTML(body);

const novel: Plugin.SourceNovel = {
Expand Down Expand Up @@ -217,27 +217,105 @@ class NovelUpdates implements Plugin.PluginBase {
chapterContent = loadedCheerio('.post-body').html()!;
break;
}
// Last edited in 0.9.2 by Batorian - 08/09/2025
case 'brightnovels': {
// Modular extraction inspired by W2e
const extractBrightNovelsContent = (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 <div id="app">
const dataPage = cheerioInstance('#app').attr('data-page');
if (!dataPage) {
throw new Error('data-page attribute not found on Bright Novels.');
}

// 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 Bright Novels.',
);
}

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 `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`;
};

try {
chapterText = extractBrightNovelsContent(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;
}
break;
}
// Last edited in 0.9.2 by Batorian - 08/09/2025
case 'canonstory': {
try {
const parts = chapterPath.split('/');
if (parts.length < 7) {
throw new Error('Invalid chapter URL structure');
}

const novelSlug = parts[4];
const chapterSlug = parts[6];
const url = `${parts[0]}//${parts[2]}/api/public/chapter-by-slug/${novelSlug}/${chapterSlug}`;

const response = await fetchApi(url);
if (!response.ok) {
throw new Error(`Failed to fetch chapter: ${response.status}`);
}

const json = await response.json();
if (!json?.data?.currentChapter) {
throw new Error('Invalid API response structure.');
}

const data = json.data.currentChapter;
const { chapterNumber, title, content } = data;

const titleElement = `Chapter ${chapterNumber}`;
chapterTitle = title ? `${titleElement} - ${title}` : titleElement;
chapterContent = content.replace(/\n/g, '<br>');
break;
} catch (error) {
throw new Error(`Failed to parse Canon Story chapter: ${error}`);
}
}
// Last edited in 0.9.0 by Batorian - 19/03/2025
case 'daoist': {
chapterTitle = loadedCheerio('.chapter__title').first().text();
chapterContent = loadedCheerio('.chapter__content').html()!;
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 <br>) after each <br> element
chapterCheerio('br').each((_, el) => {
chapterCheerio(el).after('<br>'); // Add one more <br> 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',
Expand Down Expand Up @@ -335,8 +413,8 @@ class NovelUpdates implements Plugin.PluginBase {
// Get the chapter link from the main page
const url = loadedCheerio('article > p > a').first().attr('href')!;
if (url) {
const result = await fetchApi(url);
const body = await result.text();
const response = await fetchApi(url);
const body = await response.text();
loadedCheerio = parseHTML(body);
}
chapterContent = loadedCheerio('.hentry').html()!;
Expand Down Expand Up @@ -378,6 +456,36 @@ class NovelUpdates implements Plugin.PluginBase {
}
break;
}
// Last edited in 0.9.2 by Batorian - 08/09/2025
case 'machineslicedbread': {
const urlPath = chapterPath.split('/').filter(Boolean);
const pathSegments = urlPath.slice(2);
const pathDepth = pathSegments.length;

let loadedCheerioSlicedBread = loadedCheerio;

// Handle redirect pages
if (pathDepth === 1) {
const chapterPath = loadedCheerio('.entry-content a')
.first()
.attr('href');
if (!chapterPath) {
throw new Error('Chapter path not found.');
}

const response = await fetchApi(chapterPath);
if (!response.ok) {
throw new Error(`Failed to fetch chapter: ${response.status}`);
}

const body = await response.text();
loadedCheerioSlicedBread = parseHTML(body);
}

// Extract chapter content
chapterText = loadedCheerioSlicedBread('.entry-content').html()!;
break;
}
// Last edited in 0.9.0 by Batorian - 19/03/2025
case 'mirilu': {
bloatElements = ['#jp-post-flair'];
Expand Down Expand Up @@ -542,8 +650,8 @@ class NovelUpdates implements Plugin.PluginBase {
// Get the chapter link from the main page
const url = loadedCheerio('.entry-content a').attr('href')!;
if (url) {
const result = await fetchApi(url);
const body = await result.text();
const response = await fetchApi(url);
const body = await response.text();
loadedCheerio = parseHTML(body);
}
bloatElements = [
Expand Down Expand Up @@ -587,8 +695,8 @@ class NovelUpdates implements Plugin.PluginBase {
// Get the chapter link from the main page
const url = loadedCheerio('.entry-content a').attr('href')!;
if (url) {
const result = await fetchApi(chapterPath + url);
const body = await result.text();
const response = await fetchApi(chapterPath + url);
const body = await response.text();
loadedCheerio = parseHTML(body);
}
chapterTitle = loadedCheerio('.entry-title').first().text();
Expand Down Expand Up @@ -665,9 +773,9 @@ class NovelUpdates implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
let chapterText;

const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const url = result.url;
const response = await fetchApi(this.site + chapterPath);
const body = await response.text();
const url = response.url;
const domainParts = url.toLowerCase().split('/')[2].split('.');

const loadedCheerio = parseHTML(body);
Expand All @@ -686,9 +794,9 @@ class NovelUpdates implements Plugin.PluginBase {
}

// Check if chapter url is wrong or site is down
if (!result.ok) {
if (!response.ok) {
throw new Error(
`Failed to fetch ${result.url}: ${result.status} ${result.statusText}`,
`Failed to fetch ${response.url}: ${response.status} ${response.statusText}`,
);
}

Expand Down Expand Up @@ -722,10 +830,11 @@ class NovelUpdates implements Plugin.PluginBase {
'anotivereads',
'arcanetranslations',
'asuratls',
'darkstartranslations',
'fictionread',
'helscans',
'hiraethtranslation',
'infinitenoveltranslations',
'machineslicedbread',
'mirilu',
'novelworldtranslations',
'sacredtexttranslations',
Expand All @@ -739,7 +848,7 @@ class NovelUpdates implements Plugin.PluginBase {
isBlogspot = false;
}

// Last edited in 0.9.0 - 19/03/2025
// Last edited in 0.9.2 - 08/09/2025
/**
* Blogspot sites:
* - ¼-Assed
Expand All @@ -753,20 +862,22 @@ class NovelUpdates implements Plugin.PluginBase {
* - Anomlaously Creative (Outlier)
* - Arcane Translations (Outlier)
* - Blossom Translation
* - Darkstar Translations (Outlier)
* - Dumahs Translations
* - Dumah's Translations
* - ElloMTL
* - Ether Reads
* - Femme Fables
* - Gadgetized Panda Translation
* - Gem Novels
* - Goblinslate
* - GreenzTL
* - Hel Scans (Outlier)
* - Hiraeth Translation (Outlier)
* - ippotranslations
* - JATranslations
* - Light Novels Translations
* - Machine Sliced Bread (Outlier)
* - Mirilu - Novel Reader Attempts Translating (Outlier)
* - Neosekai Translations
* - Noice Translations
* - Shanghai Fantasy
* - Soafp (Manually added)
* - Stabbing with a Syringe (Outlier)
Expand Down Expand Up @@ -819,6 +930,7 @@ class NovelUpdates implements Plugin.PluginBase {
'.wp-block-post-title',
'.title_story',
'#chapter-heading',
'.chapter-title',
'head title',
'h1:first-of-type',
'h2:first-of-type',
Expand Down Expand Up @@ -849,12 +961,13 @@ class NovelUpdates implements Plugin.PluginBase {
'#content',
'#the-content',
'article.post',
'.chp_raw',
];
const chapterContent = contentSelectors
.map(sel => loadedCheerio(sel).html()!)
.find(html => html);

if (chapterTitle) {
if (chapterTitle && chapterContent) {
chapterText = `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`;
} else {
chapterText = chapterContent;
Expand All @@ -872,7 +985,7 @@ class NovelUpdates implements Plugin.PluginBase {
// Convert relative URLs to absolute
chapterText = chapterText.replace(
/href="\//g,
`href="${this.getLocation(result.url)}/`,
`href="${this.getLocation(response.url)}/`,
);

// Process images
Expand Down Expand Up @@ -912,8 +1025,8 @@ class NovelUpdates implements Plugin.PluginBase {
searchTerm = longestSearchTerm.replace(/[‘’]/g, "'").replace(/\s+/g, '+');

const url = `${this.site}series-finder/?sf=1&sh=${searchTerm}&sort=srank&order=asc&pg=${page}`;
const result = await fetchApi(url);
const body = await result.text();
const response = await fetchApi(url);
const body = await response.text();

const loadedCheerio = parseHTML(body);

Expand Down