feat(novelbuddy): optimize performance and improve watermark removal (#2198)
* fix(novelbuddy): fix watermark regex and bump version to 2.1.2 * fix(novelbuddy): fix watermark regex, remove chapter api fetch and bump version to 2.1.2 * Update novelbuddy.ts * feat(novelbuddy): optimize performance and improve watermark removal * feat(novelbuddy): optimize performance and improve watermark removal (v2.1.2) * chore: restore original index.ts to ensure clean PR * refactor: address reviewer feedback and update watermark regex - Replace regex-based URL path manipulation with startsWith and slice. - Update fwnRegex with a more comprehensive pattern. - Run npm run format. * refactor: update watermark regex to latest comprehensive pattern - Integrate the newest provided fwnRegex pattern. - Ensure non-capturing groups and special characters are correctly handled. - Maintain addressing of reviewer feedback regarding URL manipulation. - Run npm run format.
This commit is contained in:
+82
-146
@@ -4,18 +4,21 @@ import { Plugin } from '@/types/plugin';
|
|||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
|
const fwnRegex =
|
||||||
|
/(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|\bf)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ |b)(?:ո|ռ|ח|𝒏|𝓷|𝙣|𝑛|𝖓|𝔫|𝗇|𝚗|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|延|𝐧|𝔫|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|o|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|オ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|\|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝙫|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ḷ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\||\\|Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|ލ|𝗅|𝗹|ލ|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖈|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖈|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|o|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝕞|𝕞|ⓜ|m|ന|ᙢ|൩|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|🇲|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g;
|
||||||
|
|
||||||
class NovelBuddy implements Plugin.PluginBase {
|
class NovelBuddy implements Plugin.PluginBase {
|
||||||
id = 'novelbuddy';
|
id = 'novelbuddy';
|
||||||
name = 'NovelBuddy';
|
name = 'NovelBuddy';
|
||||||
site = 'https://novelbuddy.com/';
|
site = 'https://novelbuddy.com/';
|
||||||
api = 'https://api.novelbuddy.com/';
|
api = 'https://api.novelbuddy.com/';
|
||||||
version = '2.1.1';
|
version = '2.1.2';
|
||||||
icon = 'src/en/novelbuddy/icon.png';
|
icon = 'src/en/novelbuddy/icon.png';
|
||||||
|
|
||||||
parseNovels(body: Response): Plugin.NovelItem[] {
|
parseNovels(body: Response): Plugin.NovelItem[] {
|
||||||
return body.data.items.map(item => ({
|
return body.data.items.map(item => ({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
path: new URL(item.url, this.site).pathname.substring(1),
|
path: item.url.startsWith('/') ? item.url.slice(1) : item.url,
|
||||||
cover: item.cover,
|
cover: item.cover,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -26,10 +29,8 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const { genre, min_ch, max_ch, status, demo, orderBy, keyword } = filters;
|
const { genre, min_ch, max_ch, status, demo, orderBy, keyword } = filters;
|
||||||
|
|
||||||
// Chapter bounds must be an integer between 0 and 10,000 or api cri
|
|
||||||
const parseNumber = (val?: string) => {
|
const parseNumber = (val?: string) => {
|
||||||
if (!val?.trim()) return;
|
if (!val?.trim()) return;
|
||||||
|
|
||||||
const n = Number(val);
|
const n = Number(val);
|
||||||
return Number.isInteger(n) && n >= 0 && n <= 10000
|
return Number.isInteger(n) && n >= 0 && n <= 10000
|
||||||
? String(n)
|
? String(n)
|
||||||
@@ -41,7 +42,7 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
exclude: genre.value.exclude?.join(',') || undefined,
|
exclude: genre.value.exclude?.join(',') || undefined,
|
||||||
min_ch: parseNumber(min_ch.value),
|
min_ch: parseNumber(min_ch.value),
|
||||||
max_ch: parseNumber(max_ch.value),
|
max_ch: parseNumber(max_ch.value),
|
||||||
status: String(status.value),
|
status: status.value !== 'all' ? String(status.value) : undefined,
|
||||||
demographic: demo.value?.join(',') || undefined,
|
demographic: demo.value?.join(',') || undefined,
|
||||||
sort: String(orderBy.value),
|
sort: String(orderBy.value),
|
||||||
page: String(pageNo),
|
page: String(pageNo),
|
||||||
@@ -49,15 +50,13 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
q: keyword.value || undefined,
|
q: keyword.value || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter out the undefined values
|
const params = new URLSearchParams();
|
||||||
const params = Object.fromEntries(
|
for (const [key, value] of Object.entries(rawParams)) {
|
||||||
Object.entries(rawParams).filter(([, value]) => value !== undefined),
|
if (value !== undefined) params.append(key, value);
|
||||||
) as Record<string, string>;
|
}
|
||||||
|
|
||||||
const url = new URL('titles/search', this.api);
|
const url = this.api + 'titles/search?' + params.toString();
|
||||||
url.search = new URLSearchParams(params).toString();
|
const result = await fetchApi(url);
|
||||||
|
|
||||||
const result = await fetchApi(url.toString());
|
|
||||||
const body = await result.json();
|
const body = await result.json();
|
||||||
|
|
||||||
return this.parseNovels(body);
|
return this.parseNovels(body);
|
||||||
@@ -66,14 +65,14 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const response = await fetchApi(this.site + novelPath);
|
const response = await fetchApi(this.site + novelPath);
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
const $ = parseHTML(body);
|
|
||||||
|
|
||||||
const script = $('#__NEXT_DATA__').html();
|
const scriptMatch = body.match(
|
||||||
if (!script) throw new Error('Could not find __NEXT_DATA__');
|
/<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/,
|
||||||
|
);
|
||||||
|
if (!scriptMatch) throw new Error('Could not find __NEXT_DATA__');
|
||||||
|
|
||||||
const data: NovelScript = JSON.parse(script);
|
const data: NovelScript = JSON.parse(scriptMatch[1]);
|
||||||
const initialManga = data.props.pageProps.initialManga;
|
const initialManga = data.props.pageProps.initialManga;
|
||||||
|
|
||||||
if (!initialManga) throw new Error('Could not find initialManga data');
|
if (!initialManga) throw new Error('Could not find initialManga data');
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
@@ -93,34 +92,31 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
dropped: NovelStatus.Cancelled,
|
dropped: NovelStatus.Cancelled,
|
||||||
cancelled: NovelStatus.Cancelled,
|
cancelled: NovelStatus.Cancelled,
|
||||||
completed: NovelStatus.Completed,
|
completed: NovelStatus.Completed,
|
||||||
unknown: NovelStatus.Unknown,
|
|
||||||
};
|
};
|
||||||
novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown;
|
novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown;
|
||||||
|
|
||||||
// Wrap in <div> before passing to $(): when the API returns plain text (no leading
|
const summaryStr = initialManga.summary || '';
|
||||||
// `<` tag) cheerio's $() treats the input as a CSS selector, and any `.` in the text
|
if (summaryStr) {
|
||||||
// (e.g. punctuation in "I was inside a novel.") trips the selector parser with
|
const $ = parseHTML('<div>' + summaryStr + '</div>');
|
||||||
// "Expected name, found ." and the entire parseNovel call fails. Wrapping forces the
|
$('br').replaceWith('\n');
|
||||||
// HTML-parsing branch regardless of whether the summary is plain text or HTML.
|
$('p').before('\n').after('\n\n');
|
||||||
const summary = $('<div>' + (initialManga.summary || '') + '</div>');
|
novel.summary = $('div')
|
||||||
summary.find('br').replaceWith('\n');
|
|
||||||
summary.find('p').before('\n').after('\n\n');
|
|
||||||
|
|
||||||
novel.summary =
|
|
||||||
summary
|
|
||||||
.text()
|
.text()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(line => line.trim())
|
.map(line => line.trim())
|
||||||
.join('\n')
|
.filter(line => line.length > 0)
|
||||||
?.replace(/\n{3,}/g, '\n\n')
|
.join('\n\n')
|
||||||
.trim() || 'Summary Not Found';
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (initialManga.ratingStats) {
|
if (initialManga.ratingStats) {
|
||||||
novel.rating = initialManga.ratingStats.average;
|
novel.rating = initialManga.ratingStats.average;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch full chapter list from API
|
const cv = initialManga.content_version || initialManga.cv;
|
||||||
const chaptersUrl = `${this.api}titles/${initialManga.id}/chapters`;
|
const chaptersUrl = `${this.api}titles/${initialManga.id}/chapters${
|
||||||
|
cv ? `?cv=${cv}` : ''
|
||||||
|
}`;
|
||||||
const chaptersResponse = await fetchApi(chaptersUrl);
|
const chaptersResponse = await fetchApi(chaptersUrl);
|
||||||
const chaptersJson: ChapterResponse = await chaptersResponse.json();
|
const chaptersJson: ChapterResponse = await chaptersResponse.json();
|
||||||
|
|
||||||
@@ -128,7 +124,9 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
novel.chapters = chaptersJson.data.chapters
|
novel.chapters = chaptersJson.data.chapters
|
||||||
.map(chapter => ({
|
.map(chapter => ({
|
||||||
name: chapter.name,
|
name: chapter.name,
|
||||||
path: new URL(chapter.url, this.site).pathname.substring(1),
|
path:
|
||||||
|
(chapter.url.startsWith('/') ? chapter.url.slice(1) : chapter.url) +
|
||||||
|
`?id=${initialManga.id}&chapterId=${chapter.id}`,
|
||||||
releaseTime: chapter.updated_at,
|
releaseTime: chapter.updated_at,
|
||||||
}))
|
}))
|
||||||
.reverse();
|
.reverse();
|
||||||
@@ -136,7 +134,9 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
novel.chapters = initialManga.chapters
|
novel.chapters = initialManga.chapters
|
||||||
.map(chapter => ({
|
.map(chapter => ({
|
||||||
name: chapter.name,
|
name: chapter.name,
|
||||||
path: new URL(chapter.url, this.site).pathname.substring(1),
|
path: chapter.url.startsWith('/')
|
||||||
|
? chapter.url.slice(1)
|
||||||
|
: chapter.url,
|
||||||
releaseTime: chapter.updatedAt,
|
releaseTime: chapter.updatedAt,
|
||||||
}))
|
}))
|
||||||
.reverse();
|
.reverse();
|
||||||
@@ -146,28 +146,40 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const result = await fetchApi(this.site + chapterPath);
|
const novelIdMatch = chapterPath.match(/[?&]id=([^&]+)/);
|
||||||
const body = await result.text();
|
const chapterIdMatch = chapterPath.match(/[?&]chapterId=([^&]+)/);
|
||||||
const $ = parseHTML(body);
|
|
||||||
|
|
||||||
const script = $('#__NEXT_DATA__').html();
|
let content = '';
|
||||||
if (!script) throw new Error('Could not find __NEXT_DATA__');
|
|
||||||
|
|
||||||
const data: ChapterScript = JSON.parse(script);
|
if (novelIdMatch && chapterIdMatch) {
|
||||||
const initialChapter = data.props.pageProps.initialChapter;
|
const novelId = novelIdMatch[1];
|
||||||
if (!initialChapter) throw new Error('Could not find chapter content');
|
const chapterId = chapterIdMatch[1];
|
||||||
|
const apiUrl = `${this.api}titles/${novelId}/chapters/${chapterId}`;
|
||||||
|
const response = await fetchApi(apiUrl);
|
||||||
|
const json = await response.json();
|
||||||
|
content = json?.data?.chapter?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
let content = initialChapter.content;
|
if (!content) {
|
||||||
|
const result = await fetchApi(this.site + chapterPath);
|
||||||
|
const body = await result.text();
|
||||||
|
const scriptMatch = body.match(
|
||||||
|
/<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/,
|
||||||
|
);
|
||||||
|
if (!scriptMatch) throw new Error('Could not find __NEXT_DATA__');
|
||||||
|
|
||||||
|
const data: ChapterScript = JSON.parse(scriptMatch[1]);
|
||||||
|
const initialChapter = data.props.pageProps.initialChapter;
|
||||||
|
if (!initialChapter) throw new Error('Could not find chapter content');
|
||||||
|
content = initialChapter.content;
|
||||||
|
}
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
// Remove Webnovel watermarks/ads
|
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/Find authorized novels in Webnovel.*?faster updates, better experience.*?Please click www\.webnovel\.com for visiting\./gi,
|
/Find authorized novels in Webnovel.*?faster updates, better experience.*?Please click www\.webnovel\.com for visiting\./gi,
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
|
content = content.replace(fwnRegex, '');
|
||||||
// Remove obfuscated freewebnovel watermarks (e.g., free𝑤𝑒𝑏novel.com)
|
|
||||||
content = content.replace(/free.*?novel\.com/gi, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
@@ -182,13 +194,9 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
'limit': '24',
|
'limit': '24',
|
||||||
'page': page.toString(),
|
'page': page.toString(),
|
||||||
});
|
});
|
||||||
|
const url = this.api + 'titles/search?' + params.toString();
|
||||||
const url = new URL('titles/search', this.api);
|
const result = await fetchApi(url);
|
||||||
url.search = new URLSearchParams(params).toString();
|
|
||||||
|
|
||||||
const result = await fetchApi(url.toString());
|
|
||||||
const body = await result.json();
|
const body = await result.json();
|
||||||
|
|
||||||
return this.parseNovels(body);
|
return this.parseNovels(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,11 +215,7 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
],
|
],
|
||||||
type: FilterTypes.Picker,
|
type: FilterTypes.Picker,
|
||||||
},
|
},
|
||||||
keyword: {
|
keyword: { value: '', label: 'Keywords', type: FilterTypes.TextInput },
|
||||||
value: '',
|
|
||||||
label: 'Keywords',
|
|
||||||
type: FilterTypes.TextInput,
|
|
||||||
},
|
|
||||||
status: {
|
status: {
|
||||||
value: 'all',
|
value: 'all',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
@@ -225,78 +229,48 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
type: FilterTypes.Picker,
|
type: FilterTypes.Picker,
|
||||||
},
|
},
|
||||||
genre: {
|
genre: {
|
||||||
value: {
|
value: { include: [], exclude: [] },
|
||||||
include: [],
|
|
||||||
exclude: [],
|
|
||||||
},
|
|
||||||
label: 'Genres (OR, not AND)',
|
label: 'Genres (OR, not AND)',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Action', value: 'action' },
|
{ label: 'Action', value: 'action' },
|
||||||
{ label: 'Action Adventure', value: 'action-adventure' },
|
|
||||||
{ label: 'ActionAdventure', value: 'actionadventure' },
|
{ label: 'ActionAdventure', value: 'actionadventure' },
|
||||||
{ label: 'Adult', value: 'adult' },
|
{ label: 'Adult', value: 'adult' },
|
||||||
{ label: 'Adventcure', value: 'adventcure' },
|
|
||||||
{ label: 'Adventure', value: 'adventure' },
|
{ label: 'Adventure', value: 'adventure' },
|
||||||
{ label: 'Adventurer', value: 'adventurer' },
|
|
||||||
{ label: 'Anime u0026 Comics', value: 'anime-u0026-comics' },
|
|
||||||
{ label: 'Bender', value: 'bender' },
|
|
||||||
{ label: 'Booku0026Literature', value: 'booku0026literature' },
|
|
||||||
{ label: 'Chinese', value: 'chinese' },
|
|
||||||
{ label: 'Comed', value: 'comed' },
|
|
||||||
{ label: 'Comedy', value: 'comedy' },
|
{ label: 'Comedy', value: 'comedy' },
|
||||||
{ label: 'ComedySlice of Life', value: 'comedyslice-of-life' },
|
|
||||||
{ label: 'Cultivation', value: 'cultivation' },
|
|
||||||
{ label: 'Drama', value: 'drama' },
|
{ label: 'Drama', value: 'drama' },
|
||||||
{ label: 'dventure', value: 'dventure' },
|
|
||||||
{ label: 'Eastern', value: 'eastern' },
|
{ label: 'Eastern', value: 'eastern' },
|
||||||
{ label: 'Easterni', value: 'easterni' },
|
{ label: 'Easterni', value: 'easterni' },
|
||||||
{ label: 'Ecchi', value: 'ecchi' },
|
{ label: 'Ecchi', value: 'ecchi' },
|
||||||
{ label: 'Ecchi Fantasy', value: 'ecchi-fantasy' },
|
|
||||||
{ label: 'Fan-Fiction', value: 'fan-fiction' },
|
{ label: 'Fan-Fiction', value: 'fan-fiction' },
|
||||||
{ label: 'Fanfiction', value: 'fanfiction' },
|
|
||||||
{ label: 'Fantas', value: 'fantas' },
|
|
||||||
{ label: 'Fantasy', value: 'fantasy' },
|
{ label: 'Fantasy', value: 'fantasy' },
|
||||||
{ label: 'FantasyAction', value: 'fantasyaction' },
|
|
||||||
{ label: 'Game', value: 'game' },
|
{ label: 'Game', value: 'game' },
|
||||||
{ label: 'Games', value: 'games' },
|
{ label: 'Games', value: 'games' },
|
||||||
{ label: 'Gender', value: 'gender' },
|
|
||||||
{ label: 'Gender Bender', value: 'gender-bender' },
|
{ label: 'Gender Bender', value: 'gender-bender' },
|
||||||
{ label: 'Harem', value: 'harem' },
|
{ label: 'Harem', value: 'harem' },
|
||||||
{ label: 'HaremAction', value: 'haremaction' },
|
|
||||||
{ label: 'Haremv', value: 'haremv' },
|
|
||||||
{ label: 'Historica', value: 'historica' },
|
|
||||||
{ label: 'Historical', value: 'historical' },
|
{ label: 'Historical', value: 'historical' },
|
||||||
{ label: 'History', value: 'history' },
|
|
||||||
{ label: 'Horror', value: 'horror' },
|
{ label: 'Horror', value: 'horror' },
|
||||||
{ label: 'Isekai', value: 'isekai' },
|
{ label: 'Isekai', value: 'isekai' },
|
||||||
{ label: 'Josei', value: 'josei' },
|
{ label: 'Josei', value: 'josei' },
|
||||||
{ label: 'lice of Life', value: 'lice-of-life' },
|
|
||||||
{ label: 'Light Novel', value: 'light-novel' },
|
|
||||||
{ label: 'Litrpg', value: 'litrpg' },
|
|
||||||
{ label: 'Lolicon', value: 'lolicon' },
|
{ label: 'Lolicon', value: 'lolicon' },
|
||||||
{ label: 'Magic', value: 'magic' },
|
{ label: 'Magic', value: 'magic' },
|
||||||
{ label: 'Martial', value: 'martial' },
|
|
||||||
{ label: 'Martial Arts', value: 'martial-arts' },
|
{ label: 'Martial Arts', value: 'martial-arts' },
|
||||||
{ label: 'Mature', value: 'mature' },
|
{ label: 'Mature', value: 'mature' },
|
||||||
{ label: 'Mecha', value: 'mecha' },
|
{ label: 'Mecha', value: 'mecha' },
|
||||||
{ label: 'Military', value: 'military' },
|
{ label: 'Military', value: 'military' },
|
||||||
{ label: 'Modern Life', value: 'modern-life' },
|
{ label: 'Modern Life', value: 'modern-life' },
|
||||||
{ label: 'Movies', value: 'movies' },
|
{ label: 'Movies', value: 'movies' },
|
||||||
{ label: 'Myster', value: 'myster' },
|
|
||||||
{ label: 'Mystery', value: 'mystery' },
|
{ label: 'Mystery', value: 'mystery' },
|
||||||
{ label: 'Mystery.Adventure', value: 'mystery.adventure' },
|
|
||||||
{ label: 'Psychologic', value: 'psychologic' },
|
{ label: 'Psychologic', value: 'psychologic' },
|
||||||
{ label: 'Psychological', value: 'psychological' },
|
{ label: 'Psychological', value: 'psychological' },
|
||||||
{ label: 'Reincarnatio', value: 'reincarnatio' },
|
{ label: 'Reincarnatio', value: 'reincarnatio' },
|
||||||
{ label: 'Reincarnation', value: 'reincarnation' },
|
{ label: 'Reincarnation', value: 'reincarnation' },
|
||||||
{ label: 'Romanc', value: 'romanc' },
|
{ label: 'Romanc', value: 'romanc' },
|
||||||
{ label: 'Romance', value: 'romance' },
|
{ label: 'Romance', value: 'romance' },
|
||||||
{ label: 'Romance.Adventure', value: 'romance.adventure' },
|
{ label: 'Romance.Adventure', value: 'romance-adventure' },
|
||||||
{ label: 'Romance.Harem', value: 'romance.harem' },
|
|
||||||
{ label: 'Romance.Smut', value: 'romance.smut' },
|
|
||||||
{ label: 'RomanceAction', value: 'romanceaction' },
|
|
||||||
{ label: 'RomanceAdventure', value: 'romanceadventure' },
|
{ label: 'RomanceAdventure', value: 'romanceadventure' },
|
||||||
|
{ label: 'Romance.Harem', value: 'romance-harem' },
|
||||||
{ label: 'RomanceHarem', value: 'romanceharem' },
|
{ label: 'RomanceHarem', value: 'romanceharem' },
|
||||||
|
{ label: 'Romance.Smut', value: 'romance-smut' },
|
||||||
{ label: 'Romancei', value: 'romancei' },
|
{ label: 'Romancei', value: 'romancei' },
|
||||||
{ label: 'Romancem', value: 'romancem' },
|
{ label: 'Romancem', value: 'romancem' },
|
||||||
{ label: 'School Life', value: 'school-life' },
|
{ label: 'School Life', value: 'school-life' },
|
||||||
@@ -333,8 +307,8 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
type: FilterTypes.TextInput,
|
type: FilterTypes.TextInput,
|
||||||
},
|
},
|
||||||
max_ch: {
|
max_ch: {
|
||||||
value: '',
|
|
||||||
label: 'Maximum Chapters',
|
label: 'Maximum Chapters',
|
||||||
|
value: '',
|
||||||
type: FilterTypes.TextInput,
|
type: FilterTypes.TextInput,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
@@ -364,19 +338,8 @@ class NovelBuddy implements Plugin.PluginBase {
|
|||||||
|
|
||||||
export default new NovelBuddy();
|
export default new NovelBuddy();
|
||||||
|
|
||||||
type Response = {
|
type Response = { data: { items: Items[] } };
|
||||||
data: {
|
type ChapterResponse = { success: boolean; data?: { chapters?: Items[] } };
|
||||||
items: Items[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChapterResponse = {
|
|
||||||
success: boolean;
|
|
||||||
data?: {
|
|
||||||
chapters?: Items[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Items = {
|
type Items = {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -386,16 +349,9 @@ type Items = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
|
cv?: number;
|
||||||
};
|
};
|
||||||
|
type NovelScript = { props: { pageProps: { initialManga: Manga } } };
|
||||||
type NovelScript = {
|
|
||||||
props: {
|
|
||||||
pageProps: {
|
|
||||||
initialManga: Manga;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Manga = {
|
type Manga = {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -403,34 +359,14 @@ type Manga = {
|
|||||||
altName?: string;
|
altName?: string;
|
||||||
cover: string;
|
cover: string;
|
||||||
status: string;
|
status: string;
|
||||||
ratingStats?: {
|
ratingStats?: { average: number };
|
||||||
average: number;
|
|
||||||
};
|
|
||||||
summary?: string;
|
summary?: string;
|
||||||
artists?: {
|
artists?: { name: string; slug: string }[];
|
||||||
name: string;
|
authors?: { name: string; slug: string }[];
|
||||||
slug: string;
|
genres?: { name: string; slug: string }[];
|
||||||
}[];
|
|
||||||
authors?: {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}[];
|
|
||||||
genres?: {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}[];
|
|
||||||
chapters?: Items[];
|
chapters?: Items[];
|
||||||
|
cv?: number;
|
||||||
|
content_version?: number;
|
||||||
};
|
};
|
||||||
|
type ChapterScript = { props: { pageProps: { initialChapter: Chapter } } };
|
||||||
type ChapterScript = {
|
type Chapter = { name: string; content: string };
|
||||||
props: {
|
|
||||||
pageProps: {
|
|
||||||
initialChapter: Chapter;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Chapter = {
|
|
||||||
name: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user