From c30e2b5966b5189c1f2e6466946b3378a07423af Mon Sep 17 00:00:00 2001
From: Lukas Richter <53831711+Batorian@users.noreply.github.com>
Date: Mon, 29 Apr 2024 04:32:38 +0200
Subject: [PATCH] Enhancement: Novel Updates (#915)
* refactored and updated Novel Updates' parseChapter()
* added `ippotranslations` and `shanghaifantasy` to Novel Updates
* small changes
* removed switch cases and reverted back to `let` and `if`/`else if` structure
* added lots of translation group support
* added more site support and tried cleaning chapter content where possible
* quick fix
* quick fix
* logic overhaul; implementation of switch cases (again)
* small changes to `bloatClasses` and `chapterContent`
* added Anomalously Creative (`anotivereads`) to outliers and added new `bloatClasses` to Wuxia World (`wuxiaworld`)
---
plugins/english/novelupdates.ts | 394 ++++++++++++++++++++++----------
1 file changed, 276 insertions(+), 118 deletions(-)
diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts
index 183fabe..bb96a14 100644
--- a/plugins/english/novelupdates.ts
+++ b/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.5.2';
+ version = '0.5.3';
icon = 'src/en/novelupdates/icon.png';
site = 'https://www.novelupdates.com/';
@@ -165,58 +165,200 @@ class NovelUpdates implements Plugin.PluginBase {
return match && `${match[1]}//${match[3]}`;
}
+ async getChapterBody(
+ loadedCheerio: CheerioAPI,
+ domain: string[],
+ url: string,
+ ) {
+ let bloatClasses = [];
+ let chapterTitle = '';
+ let chapterContent = '';
+ let chapterText = '';
+
+ const unwanted = ['blogspot', 'wordpress', 'www'];
+ const targetDomain = domain.find(d => !unwanted.includes(d));
+
+ switch (targetDomain) {
+ case 'anotivereads':
+ chapterTitle = loadedCheerio('#comic-nav-name').first().text()!;
+ chapterContent = loadedCheerio('#spliced-comic').html()!;
+ chapterText = `
${chapterTitle}
${chapterContent}`;
+ break;
+ case 'asuratls':
+ let titleElementAsura = loadedCheerio('.post-body div b').first();
+ chapterTitle = titleElementAsura.first().text();
+ titleElementAsura.remove();
+ chapterContent = loadedCheerio('.post-body').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'hiraethtranslation':
+ chapterTitle = loadedCheerio('li.active').first().text()!;
+ chapterContent = loadedCheerio('.text-left').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'hostednovel':
+ chapterTitle = loadedCheerio('#chapter-title').first().text()!;
+ chapterContent = loadedCheerio('#chapter-content').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'inoveltranslation':
+ const link_inovel = 'https://api.' + url.slice(8);
+ const json_inovel = await fetchApi(link_inovel).then(r => r.json());
+ chapterTitle =
+ 'Chapter ' + json_inovel.chapter + ' | ' + json_inovel.title;
+ chapterContent = json_inovel.content.replace(/\n/g, '
');
+ if (json_inovel.notes) {
+ chapterContent +=
+ '
TL Notes:
' +
+ json_inovel.notes.replace(/\n/g, '
');
+ }
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'ko-fi':
+ chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")')
+ .html()
+ ?.match(/shadowDom\.innerHTML \+= '( loadedCheerio(tag).remove());
+ chapterTitle = loadedCheerio('.entry-title').first().text()!;
+ chapterContent = loadedCheerio('.entry-content p span span').html()!;
+ let chapterContentArray = chapterContent.split('\n').map(paragraph => {
+ return paragraph.replace(/ /g, '').trim() + '
';
+ });
+ chapterContent = chapterContentArray.join('').trim();
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'raeitranslations':
+ const parts = url.split('/');
+ const link_raei = `${parts[0]}//api.${parts[2]}/api/chapters/?id=${parts[3]}&num=${parts[4]}`;
+ const json = await fetchApi(link_raei).then(r => r.json());
+ chapterTitle =
+ json.currentChapter.novTitle +
+ ' | ' +
+ 'Chapter ' +
+ json.currentChapter.num;
+ chapterContent =
+ json.currentChapter.head +
+ `
` +
+ json.currentChapter.body +
+ `
Translator's Note:
` +
+ json.currentChapter.note;
+ chapterContent = chapterContent.replace(/\n/g, '
');
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'rainofsnow':
+ loadedCheerio('.responsivevoice-button').remove();
+ chapterText = loadedCheerio('div.content').html()!;
+ break;
+ case 'sacredtexttranslations':
+ bloatClasses = ['.entry-content blockquote', '.entry-content div'];
+ bloatClasses.map(tag => loadedCheerio(tag).remove());
+ chapterTitle = loadedCheerio('.entry-title').first().text()!;
+ chapterContent = loadedCheerio('.entry-content').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'scribblehub':
+ chapterTitle = loadedCheerio('.chapter-title').first().text()!;
+ chapterContent = loadedCheerio('div.chp_raw').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'stabbingwithasyringe':
+ /**
+ * Get the chapter link from the main page
+ */
+ const linkSyringe = loadedCheerio('.entry-content a').attr('href')!;
+ const resultSyringe = await fetchApi(linkSyringe);
+ const bodySyringe = await resultSyringe.text();
+ const loadedCheerioSyringe = parseHTML(bodySyringe);
+ bloatClasses = ['.wp-block-buttons', '#jp-post-flair'];
+ bloatClasses.map(tag => loadedCheerioSyringe(tag).remove());
+ chapterContent = loadedCheerioSyringe('.entry-content').html()!;
+ let titleElementSyringe =
+ loadedCheerioSyringe('.entry-content h3').first();
+ if (titleElementSyringe.length) {
+ chapterTitle = titleElementSyringe.first().text();
+ titleElementSyringe.remove();
+ chapterContent = loadedCheerioSyringe('.entry-content').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ } else {
+ chapterText = chapterContent;
+ }
+ break;
+ case 'tinytranslation':
+ bloatClasses = [
+ '.google_translate_element',
+ '.navigate',
+ '.post-views',
+ ];
+ bloatClasses.map(tag => loadedCheerio(tag).remove());
+ chapterTitle = loadedCheerio('.title-content').first().text()!;
+ loadedCheerio('.title-content').first().remove();
+ chapterContent = loadedCheerio('.content').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'tumblr':
+ chapterText = loadedCheerio('.post').html()!;
+ break;
+ case 'wattpad':
+ chapterTitle = loadedCheerio('.h2').first().text()!;
+ chapterContent = loadedCheerio('.part-content pre').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'webnovel':
+ chapterTitle = loadedCheerio('.cha-tit .pr .dib').first().text()!;
+ chapterContent = loadedCheerio('.cha-words').html()!;
+ if (!chapterContent) {
+ chapterContent = loadedCheerio('._content').html()!;
+ }
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ case 'wuxiaworld':
+ bloatClasses = ['.MuiLink-root'];
+ bloatClasses.map(tag => loadedCheerio(tag).remove());
+ chapterTitle = loadedCheerio('h4 span').first().text()!;
+ chapterContent = loadedCheerio('.chapter-content').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ break;
+ }
+ return chapterText;
+ }
+
async parseChapter(chapterPath: string): Promise {
+ let bloatClasses = [];
+ let chapterTitle = '';
+ let chapterContent = '';
let chapterText = '';
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
+ const url = result.url.toLowerCase();
+ const domain = url.split('/')[2].split('.');
// console.log(result.url);
// console.log('Redirected URL: ', result.chapterUrl);
const loadedCheerio = parseHTML(body);
-
- let isWuxiaWorld = result.url.toLowerCase().includes('wuxiaworld');
-
- let isBlogspot = result.url.toLowerCase().includes('blogspot');
-
- let isTumblr = result.url.toLowerCase().includes('tumblr');
-
- let isWattpad = result.url.toLowerCase().includes('wattpad');
-
- let isAnomalously = result.url.toLowerCase().includes('anotivereads');
-
- let kofi = result.url.toLowerCase().includes('ko-fi');
-
- let isBlossomTranslation = result.url
- .toLowerCase()
- .includes('blossomtranslation');
-
- let isLightNovelsTls = result.url
- .toLowerCase()
- .includes('lightnovelstranslations');
-
- let isiNovelTranslation = result.url
- .toLowerCase()
- .includes('inoveltranslation');
-
- let isTravisTranslation = result.url
- .toLowerCase()
- .includes('travistranslations');
-
- let isRaeiTranslation = result.url
- .toLowerCase()
- .includes('raeitranslations');
/**
- * Checks if its a wordpress site
+ * Generators are last
*/
+ let isBlogspotStr = loadedCheerio(
+ 'meta[name="google-adsense-platform-domain"]',
+ ).attr('content');
+
+ let isBlogspot = false;
+ if (isBlogspotStr) {
+ isBlogspot = isBlogspotStr.toLowerCase().includes('blogspot');
+ }
+
let isWordPressStr =
+ loadedCheerio('#dcl_comments-js-extra').html()! ||
loadedCheerio('meta[name="generator"]').attr('content') ||
loadedCheerio('footer').text();
let isWordPress = false;
-
if (isWordPressStr) {
isWordPress =
isWordPressStr.toLowerCase().includes('wordpress') ||
@@ -224,109 +366,125 @@ class NovelUpdates implements Plugin.PluginBase {
loadedCheerio('.powered-by').text().toLowerCase().includes('wordpress');
}
- let isRainOfSnow = result.url.toLowerCase().includes('rainofsnow');
-
- let isWebNovel = result.url.toLowerCase().includes('webnovel');
-
- let isHostedNovel = result.url.toLowerCase().includes('hostednovel');
-
- let isScribbleHub = result.url.toLowerCase().includes('scribblehub');
-
- if (isWuxiaWorld) {
- chapterText = loadedCheerio('#chapter-content').html()!;
- } else if (isRainOfSnow) {
- chapterText = loadedCheerio('div.content').html()!;
- } else if (isTumblr) {
- chapterText = loadedCheerio('.post').html()!;
+ const outliers = [
+ 'anotivereads',
+ 'asuratls',
+ 'novelworldtranslations',
+ 'sacredtexttranslations',
+ 'stabbingwithasyringe',
+ 'tinytranslation',
+ ];
+ if (domain.find(d => outliers.includes(d))) {
+ isWordPress = false;
+ isBlogspot = false;
+ }
+ /**
+ * Blogspot Novels:
+ * - AssedTL
+ * - AsuraTls (Outlier)
+ * - Novel World Translations (Outlier)
+ * - SacredText Translation (Outlier)
+ *
+ * WordPress Novels:
+ * - A Novel Reader Attempts Translating
+ * - Anomalously Creative (Outlier)
+ * - Arcane Translations
+ * - Blossom Translation
+ * - Dumah's Translations
+ * - ElloTL
+ * - Gem Novels Translations
+ * - Genesis Translations
+ * - Goblinslate
+ * - Hel Scans
+ * - Ippo Translations
+ * - JATranslations
+ * - Light Novels Translations
+ * - Neosekai Translations
+ * - Shanghai Fantasy
+ * - Soafp Translations
+ * - Stabbing with a Syringe (Outlier)
+ * - Stone Scape
+ * - Tiny Translation (Outlier)
+ * - Wonder Novels
+ * - Yong Library
+ * - Zetro Translation
+ */
+ if (!isWordPress && !isBlogspot) {
+ chapterText = await this.getChapterBody(loadedCheerio, domain, url);
} else if (isBlogspot) {
- loadedCheerio('.post-share-buttons').remove();
- chapterText =
- loadedCheerio('.entry-content').html() ||
- loadedCheerio('#post-body').html()!;
- } else if (isHostedNovel) {
- chapterText = loadedCheerio('.chapter').html()!;
- } else if (isScribbleHub) {
- chapterText = loadedCheerio('div.chp_raw').html()!;
- } else if (isWattpad) {
- chapterText = loadedCheerio('.container pre').html()!;
- } else if (isTravisTranslation) {
- chapterText = loadedCheerio('.reader-content').html()!;
- } else if (isLightNovelsTls) {
- chapterText = loadedCheerio('.text_story').html()!;
- } else if (isBlossomTranslation) {
- chapterText = loadedCheerio('.manga-child-content').html()!;
- } else if (isAnomalously) {
- chapterText = loadedCheerio('#comic').html()!;
- } else if (isRaeiTranslation) {
- const parts = result.url.split('/');
- const link = `${parts[0]}//api.${parts[2]}/api/chapters/?id=${parts[3]}&num=${parts[4]}`;
- const json = await fetchApi(link).then(r => r.json());
- chapterText =
- json.currentChapter.head +
- `
` +
- json.currentChapter.body +
- `
Translator's Note:
` +
- json.currentChapter.note;
- chapterText = chapterText.replace(/\n/g, '
');
- } else if (kofi) {
- chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")')
- .html()
- ?.match(/shadowDom\.innerHTML \+= '( r.json());
- chapterText = json.content.replace(/\n/g, '
');
- if (json.notes) {
- chapterText +=
- '
TL Notes:
' + json.notes.replace(/\n/g, '
');
- }
- } else if (isWordPress) {
/**
- * Remove wordpress bloat tags
+ * Generators are last
*/
-
- const bloatClasses = [
- '.c-ads',
- '#madara-comments',
- '#comments',
- '.content-comments',
+ bloatClasses = ['.button-container', '.separator'];
+ bloatClasses.map(tag => loadedCheerio(tag).remove());
+ chapterTitle =
+ loadedCheerio('.entry-title').first().text() ||
+ loadedCheerio('.post-title').first().text()!;
+ chapterContent =
+ loadedCheerio('.entry-content').html() ||
+ loadedCheerio('.post-body').html()!;
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ } else if (isWordPress) {
+ bloatClasses = [
+ '.ad',
+ '.author-avatar',
+ '.chapter-warning',
+ '.entry-content .has-text-align-center',
+ '.entry-meta',
+ '.ezoic-ad',
+ '.mb-center',
+ '.patreon-widget',
+ '.post-cats',
+ '.pre-bar',
'.sharedaddy',
- '.wp-dark-mode-switcher',
- '.wp-next-post-navi',
+ '.sidebar',
'.wp-block-buttons',
'.wp-block-columns',
- '.post-cats',
- '.sidebar',
- '.author-avatar',
- '.ezoic-ad',
+ '.wp-block-image',
+ '.wp-dark-mode-switcher',
+ '.wp-next-post-navi',
+ '#jp-post-flair',
+ '#textbox',
];
-
bloatClasses.map(tag => loadedCheerio(tag).remove());
-
- chapterText =
+ chapterTitle =
+ loadedCheerio('.entry-title').first().text() ||
+ loadedCheerio('.entry-title-main').first().text() ||
+ loadedCheerio('.chapter__title').first().text() ||
+ loadedCheerio('.sp-title').first().text() ||
+ loadedCheerio('.title-content').first().text() ||
+ loadedCheerio('.wp-block-post-title').first().text() ||
+ loadedCheerio('.title_story').first().text() ||
+ loadedCheerio('.active').first().text() ||
+ loadedCheerio('head title').first().text()!;
+ let chapterSubtitle = loadedCheerio('.cat-series').first().text() || '';
+ chapterContent =
+ loadedCheerio('.rdminimal').html() ||
loadedCheerio('.entry-content').html() ||
+ loadedCheerio('.chapter__content').html() ||
+ loadedCheerio('.prevent-select').html() ||
+ loadedCheerio('.text_story').html() ||
+ loadedCheerio('.contenta').html() ||
loadedCheerio('.single_post').html() ||
loadedCheerio('.post-entry').html() ||
loadedCheerio('.main-content').html() ||
- loadedCheerio('article.post').html() ||
loadedCheerio('.content').html() ||
- loadedCheerio('#content').html() ||
loadedCheerio('.page-body').html() ||
- loadedCheerio('.td-page-content').html()!;
- } else if (isWebNovel) {
- chapterText = loadedCheerio('.cha-words').html()!;
-
- if (!chapterText) {
- chapterText = loadedCheerio('._content').html()!;
+ loadedCheerio('.td-page-content').html() ||
+ loadedCheerio('#content').html() ||
+ loadedCheerio('article.post').html()!;
+ if (chapterSubtitle) {
+ chapterTitle = `${chapterTitle} | ${chapterSubtitle}`;
}
- } else {
+ chapterText = `${chapterTitle}
${chapterContent}`;
+ }
+
+ if (!chapterText) {
/**
* Remove unnecessary tags
*/
const tags = ['nav', 'header', 'footer', '.hidden'];
-
tags.map(tag => loadedCheerio(tag).remove());
-
chapterText = loadedCheerio('body').html()!;
}