From 37c5b137933e8f6a4199c96ba3e418d840cb5391 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 17:00:05 +0200 Subject: [PATCH 01/15] added mythoriatales --- package.json | 2 +- plugins/english/novelupdates.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fd1141596..85508263c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnreader-plugins", - "version": "3.0.0", + "version": "3.0.1", "description": "Plugins repo for LNReader", "main": "index.js", "type": "module", diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index a824f5f4a..271e8e3a6 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.8'; + version = '0.9.9'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -513,6 +513,12 @@ class NovelUpdates implements Plugin.PluginBase { chapterContent = loadedCheerio('.entry-content').html()!; break; } + // Last edited in 0.9.9 by Batorian - 06/05/2026 + case 'mythoriatales': { + chapterTitle = loadedCheerio('h3').first().text(); + chapterContent = loadedCheerio('article').first().html()!; + break; + } // Last edited in 0.9.0 by Batorian - 19/03/2025 case 'novelplex': { bloatElements = ['.passingthrough_adreminder']; From 753ee2bae36e0be49661eb36ae284bb2bd0f5a47 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 17:31:49 +0200 Subject: [PATCH 02/15] update Co-authored-by: Copilot --- plugins/english/novelupdates.ts | 72 +++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 271e8e3a6..210518ab5 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.9'; + version = '0.9.10'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -515,8 +515,74 @@ class NovelUpdates implements Plugin.PluginBase { } // Last edited in 0.9.9 by Batorian - 06/05/2026 case 'mythoriatales': { - chapterTitle = loadedCheerio('h3').first().text(); - chapterContent = loadedCheerio('article').first().html()!; + try { + // Fetch the RSC payload directly (most reliable) + const rscUrl = `${chapterPath}?_rsc=1`; + + const response = await fetchApi(rscUrl, { + headers: { + 'Accept': 'text/x-component', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + }); + + const rscText = await response.text(); + + // Extract the chapter content - it's usually the big string after `2:T...` + // Pattern: 2:Txxxx,Actual Chapter Text Here... + let chapterContent = ''; + + const contentMatch = rscText.match( + /2:T[\w-]+,([\s\S]*?)(?=\n\d+:|\n[0-9a-f]+:|$)/, + ); + + if (contentMatch && contentMatch[1]) { + chapterContent = contentMatch[1].trim(); + } else { + // Fallback: find the largest text block (the actual story) + const largeTextMatch = rscText.match( + /"content":"([\s\S]*?)","prevChapter"/, + ); + if (largeTextMatch) { + chapterContent = largeTextMatch[1]; + } else { + // Ultimate fallback + chapterContent = rscText.replace(/^\d+:[\s\S]*?\n/, '').trim(); + } + } + + // Clean up escaped characters and format + chapterContent = chapterContent + .replace(/\\n/g, '\n') + .replace(/\\"/g, '"') + .trim(); + + // Convert newlines to paragraphs + const paragraphs = chapterContent + .split(/\n\s*\n+/) + .map(p => p.trim()) + .filter(p => p.length > 0) + .map(p => `

${p}

`) + .join('\n'); + + chapterTitle = + loadedCheerio('h1, h2, h3').first().text().trim() || + rscText.match(/title":"([^"]+)"/)?.[1] || + 'Chapter'; + + chapterText = `

${chapterTitle}



${paragraphs}`; + } catch (error) { + console.error('MythoriaTales RSC fetch failed:', error); + + // Fallback to normal HTML parsing + chapterTitle = loadedCheerio('h1, h2, h3').first().text().trim(); + chapterContent = loadedCheerio('article, main').first().html() || ''; + chapterText = chapterContent + ? `

${chapterTitle}



${chapterContent}` + : loadedCheerio('body').html() || ''; + } + break; } // Last edited in 0.9.0 by Batorian - 19/03/2025 From 5428ed303dc6a3821a91cbf5604eb4a59e282086 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 17:39:36 +0200 Subject: [PATCH 03/15] remove chapterText Co-authored-by: Copilot --- plugins/english/novelupdates.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 210518ab5..695bd9da7 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.10'; + version = '0.9.11'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -559,7 +559,7 @@ class NovelUpdates implements Plugin.PluginBase { .trim(); // Convert newlines to paragraphs - const paragraphs = chapterContent + chapterContent = chapterContent .split(/\n\s*\n+/) .map(p => p.trim()) .filter(p => p.length > 0) @@ -570,17 +570,8 @@ class NovelUpdates implements Plugin.PluginBase { loadedCheerio('h1, h2, h3').first().text().trim() || rscText.match(/title":"([^"]+)"/)?.[1] || 'Chapter'; - - chapterText = `

${chapterTitle}



${paragraphs}`; } catch (error) { - console.error('MythoriaTales RSC fetch failed:', error); - - // Fallback to normal HTML parsing - chapterTitle = loadedCheerio('h1, h2, h3').first().text().trim(); - chapterContent = loadedCheerio('article, main').first().html() || ''; - chapterText = chapterContent - ? `

${chapterTitle}



${chapterContent}` - : loadedCheerio('body').html() || ''; + throw new Error(`MythoriaTales RSC fetch failed: ${error}`); } break; From 0eea400de3e4fe075dff645c8ece777d2884d2d9 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 22:48:39 +0200 Subject: [PATCH 04/15] Claude rewrite --- plugins/english/novelupdates.ts | 105 ++++++++++++++++---------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 695bd9da7..a6f099e5e 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.11'; + version = '0.9.12'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -515,65 +515,64 @@ class NovelUpdates implements Plugin.PluginBase { } // Last edited in 0.9.9 by Batorian - 06/05/2026 case 'mythoriatales': { - try { - // Fetch the RSC payload directly (most reliable) - const rscUrl = `${chapterPath}?_rsc=1`; - - const response = await fetchApi(rscUrl, { - headers: { - 'Accept': 'text/x-component', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - }, - }); + /** + * Mythoria Tales uses Next.js Server Actions for chapter delivery. + * Confirmed via HAR: POST to the chapter URL with a stable next-action + * hash (getChapterBySlugAction). Body is ["slug", chapterNumber]. + * Response is a text/x-component stream; content follows "N:T{hexLen}," + * + * next-router-state-tree header is not required (confirmed skippable). + * next-action hash is compiled into the JS bundle per deployment: + * dpl_6jZ5WV3pQkiMWUU8a6kk6RXZN1nG → 6047fe8d21566eb56a426de4d5d5ee1eb7e01091f9 + */ + const ACTION_HASH = '6047fe8d21566eb56a426de4d5d5ee1eb7e01091f9'; + + // chapterPath is the full URL, e.g.: + // https://www.mythoriatales.com/series/my-sexy-college-girlfriends/chapter/123 + const urlParts = chapterPath.split('/'); + const slug = urlParts[4]; // "my-sexy-college-girlfriends" + const chapterNum = parseInt(urlParts[6], 10); // 123 — must be a number, not a string + + const response = await fetchApi(chapterPath, { + method: 'POST', + headers: { + 'Accept': 'text/x-component', + 'Content-Type': 'text/plain;charset=UTF-8', + 'next-action': ACTION_HASH, + }, + body: JSON.stringify([slug, chapterNum]), + }); - const rscText = await response.text(); + if (!response.ok) { + throw new Error(`Failed to fetch chapter: ${response.status}`); + } - // Extract the chapter content - it's usually the big string after `2:T...` - // Pattern: 2:Txxxx,Actual Chapter Text Here... - let chapterContent = ''; + const rscText = await response.text(); - const contentMatch = rscText.match( - /2:T[\w-]+,([\s\S]*?)(?=\n\d+:|\n[0-9a-f]+:|$)/, + // Locked/paid chapters return a short response with no content line + if (!rscText.includes(':T')) { + throw new Error( + 'This chapter is locked. Please open in webview and log in.', ); + } - if (contentMatch && contentMatch[1]) { - chapterContent = contentMatch[1].trim(); - } else { - // Fallback: find the largest text block (the actual story) - const largeTextMatch = rscText.match( - /"content":"([\s\S]*?)","prevChapter"/, - ); - if (largeTextMatch) { - chapterContent = largeTextMatch[1]; - } else { - // Ultimate fallback - chapterContent = rscText.replace(/^\d+:[\s\S]*?\n/, '').trim(); - } - } - - // Clean up escaped characters and format - chapterContent = chapterContent - .replace(/\\n/g, '\n') - .replace(/\\"/g, '"') - .trim(); - - // Convert newlines to paragraphs - chapterContent = chapterContent - .split(/\n\s*\n+/) - .map(p => p.trim()) - .filter(p => p.length > 0) - .map(p => `

${p}

`) - .join('\n'); - - chapterTitle = - loadedCheerio('h1, h2, h3').first().text().trim() || - rscText.match(/title":"([^"]+)"/)?.[1] || - 'Chapter'; - } catch (error) { - throw new Error(`MythoriaTales RSC fetch failed: ${error}`); + // Extract content after "N:T{hexLen}," + const contentMatch = rscText.match(/^\d+:T[0-9a-f]+,([\s\S]+)/m); + if (!contentMatch) { + throw new Error( + 'Could not parse chapter content from server response.', + ); } + // Convert plain-text paragraphs (separated by blank lines) to HTML + chapterContent = contentMatch[1] + .split(/\n\s*\n/) + .map((p: string) => p.trim()) + .filter((p: string) => p.length > 0) + .map((p: string) => `

${p}

`) + .join('\n'); + + chapterTitle = `Chapter ${chapterNum}`; break; } // Last edited in 0.9.0 by Batorian - 19/03/2025 From bde9d4e5aefd9787a176e8d4651b8abb74828c32 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 23:05:09 +0200 Subject: [PATCH 05/15] fix metadata included --- plugins/english/novelupdates.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index a6f099e5e..614269fb3 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.12'; + version = '0.9.13'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -556,16 +556,19 @@ class NovelUpdates implements Plugin.PluginBase { ); } - // Extract content after "N:T{hexLen}," - const contentMatch = rscText.match(/^\d+:T[0-9a-f]+,([\s\S]+)/m); + // Extract content after "N:T{hexLen},", stopping before the next RSC line + // (the trailing "1:{...}" metadata line must not be included) + const contentMatch = rscText.match(/^\d+:T([0-9a-f]+),([\s\S]*)/m); if (!contentMatch) { throw new Error( 'Could not parse chapter content from server response.', ); } + const byteLength = parseInt(contentMatch[1], 16); // Convert plain-text paragraphs (separated by blank lines) to HTML - chapterContent = contentMatch[1] + chapterText = contentMatch[2] + .slice(0, byteLength) .split(/\n\s*\n/) .map((p: string) => p.trim()) .filter((p: string) => p.length > 0) From daae415af057a8512a30d91dc1698b365a80a793 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 23:16:32 +0200 Subject: [PATCH 06/15] update chapterContent detection --- plugins/english/novelupdates.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 614269fb3..12021ce72 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.13'; + version = '0.9.14'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -549,33 +549,26 @@ class NovelUpdates implements Plugin.PluginBase { const rscText = await response.text(); - // Locked/paid chapters return a short response with no content line - if (!rscText.includes(':T')) { - throw new Error( - 'This chapter is locked. Please open in webview and log in.', - ); - } - - // Extract content after "N:T{hexLen},", stopping before the next RSC line - // (the trailing "1:{...}" metadata line must not be included) - const contentMatch = rscText.match(/^\d+:T([0-9a-f]+),([\s\S]*)/m); - if (!contentMatch) { + const line = rscText.split('\n').find(l => l.startsWith('2:')); + if (!line) { throw new Error( 'Could not parse chapter content from server response.', ); } - const byteLength = parseInt(contentMatch[1], 16); - // Convert plain-text paragraphs (separated by blank lines) to HTML - chapterText = contentMatch[2] - .slice(0, byteLength) - .split(/\n\s*\n/) + // Strip the "2:T{hexLen}," prefix + const content = line.replace(/^2:T[0-9a-f]+,/, ''); + + // First paragraph is the chapter title, rest is the body + const [title, ...paragraphs] = content.split(/\n\s*\n/); + + chapterTitle = title.trim(); + chapterContent = paragraphs .map((p: string) => p.trim()) .filter((p: string) => p.length > 0) .map((p: string) => `

${p}

`) .join('\n'); - chapterTitle = `Chapter ${chapterNum}`; break; } // Last edited in 0.9.0 by Batorian - 19/03/2025 From d914bd856727bb79e0a379bd9b6e4d7502abcbba Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 23:22:52 +0200 Subject: [PATCH 07/15] fix --- plugins/english/novelupdates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 12021ce72..3a6598320 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.14'; + version = '0.9.15'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -560,7 +560,7 @@ class NovelUpdates implements Plugin.PluginBase { const content = line.replace(/^2:T[0-9a-f]+,/, ''); // First paragraph is the chapter title, rest is the body - const [title, ...paragraphs] = content.split(/\n\s*\n/); + const [title, ...paragraphs] = content.split(/(?:\\n\s*)+/); chapterTitle = title.trim(); chapterContent = paragraphs From 151edbe90a3695d708214b7eb8e9b7150857b708 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 23:36:59 +0200 Subject: [PATCH 08/15] Gemini chapterContent rework --- plugins/english/novelupdates.ts | 53 +++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 3a6598320..6e4db45d3 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.15'; + version = '0.9.16'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -549,24 +549,51 @@ class NovelUpdates implements Plugin.PluginBase { const rscText = await response.text(); - const line = rscText.split('\n').find(l => l.startsWith('2:')); - if (!line) { + /** + * 1. Split the response into segments based on the Next.js RSC protocol. + * Each segment starts with a newline followed by an index and a type + * marker (T for Text, { for JSON, E for Error). + * This prevents "1:" inside a sentence from breaking the split. + */ + const segments = rscText.split(/\n(?=\d+:[{TE])/); + + // 2. Find the segment that contains the chapter text (prefixed with 2:T) + const contentSegment = segments.find(s => s.startsWith('2:T')); + + if (!contentSegment) { throw new Error( - 'Could not parse chapter content from server response.', + 'Could not find the chapter content segment in the response.', ); } - // Strip the "2:T{hexLen}," prefix - const content = line.replace(/^2:T[0-9a-f]+,/, ''); + /** + * 3. Clean the segment. + * Remove the "2:T{hexLen}," prefix to get the raw text. + */ + const rawText = contentSegment.replace(/^2:T[0-9a-f]+,/, '').trim(); + + /** + * 4. Split the text into lines. + * We handle literal newlines and escaped (\n) newlines, + * while filtering out empty lines caused by double-spacing. + */ + const lines = rawText + .split(/(?:\r?\n|\\n)+/) + .map(line => line.trim()) + .filter(line => line.length > 0); + + if (lines.length === 0) { + throw new Error('Chapter content is empty after parsing.'); + } - // First paragraph is the chapter title, rest is the body - const [title, ...paragraphs] = content.split(/(?:\\n\s*)+/); + // 5. Extract Title and Content + // The first line is always the title in this payload structure + chapterTitle = lines[0]; - chapterTitle = title.trim(); - chapterContent = paragraphs - .map((p: string) => p.trim()) - .filter((p: string) => p.length > 0) - .map((p: string) => `

${p}

`) + // All subsequent lines are paragraphs + chapterContent = lines + .slice(1) + .map(p => `

${p}

`) .join('\n'); break; From 10788a648327cef9b769ae2945377b1af989578c Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 6 May 2026 23:52:11 +0200 Subject: [PATCH 09/15] optimize chapterContent extraction --- plugins/english/novelupdates.ts | 53 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 6e4db45d3..20631e12f 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.16'; + version = '0.9.17'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -517,21 +517,19 @@ class NovelUpdates implements Plugin.PluginBase { case 'mythoriatales': { /** * Mythoria Tales uses Next.js Server Actions for chapter delivery. - * Confirmed via HAR: POST to the chapter URL with a stable next-action - * hash (getChapterBySlugAction). Body is ["slug", chapterNumber]. - * Response is a text/x-component stream; content follows "N:T{hexLen}," + * The response is a 'text/x-component' stream (RSC). * - * next-router-state-tree header is not required (confirmed skippable). - * next-action hash is compiled into the JS bundle per deployment: - * dpl_6jZ5WV3pQkiMWUU8a6kk6RXZN1nG → 6047fe8d21566eb56a426de4d5d5ee1eb7e01091f9 + * Payload Structure: + * 0:{"a":"$@1",...} -> Initialization/Metadata + * 2:T{hexLen},... -> Chapter Body (Title on Line 1, Content below) + * 1:{"success":...} -> Series/Chapter JSON metadata */ const ACTION_HASH = '6047fe8d21566eb56a426de4d5d5ee1eb7e01091f9'; - // chapterPath is the full URL, e.g.: - // https://www.mythoriatales.com/series/my-sexy-college-girlfriends/chapter/123 + // chapterPath: https://www.mythoriatales.com/series/[slug]/chapter/[num] const urlParts = chapterPath.split('/'); - const slug = urlParts[4]; // "my-sexy-college-girlfriends" - const chapterNum = parseInt(urlParts[6], 10); // 123 — must be a number, not a string + const slug = urlParts[4]; + const chapterNum = parseInt(urlParts[6], 10); const response = await fetchApi(chapterPath, { method: 'POST', @@ -550,32 +548,33 @@ class NovelUpdates implements Plugin.PluginBase { const rscText = await response.text(); /** - * 1. Split the response into segments based on the Next.js RSC protocol. - * Each segment starts with a newline followed by an index and a type - * marker (T for Text, { for JSON, E for Error). - * This prevents "1:" inside a sentence from breaking the split. + * 1. Isolate the data segments. + * We split by newline followed by a digit and a type marker (:, {, T). + * Using a lookahead (?=...) ensures the split marker isn't consumed, + * allowing us to verify the segment index (e.g., "2:"). */ const segments = rscText.split(/\n(?=\d+:[{TE])/); - // 2. Find the segment that contains the chapter text (prefixed with 2:T) + // 2. Locate the text segment (usually index 2) const contentSegment = segments.find(s => s.startsWith('2:T')); if (!contentSegment) { throw new Error( - 'Could not find the chapter content segment in the response.', + 'Could not find the chapter content segment (2:T) in the stream.', ); } /** - * 3. Clean the segment. - * Remove the "2:T{hexLen}," prefix to get the raw text. + * 3. Extract and Clean Raw Text + * Removes the "2:T{hexLen}," prefix. + * [0-9a-f]+ handles the hexadecimal length provided by the RSC protocol. */ const rawText = contentSegment.replace(/^2:T[0-9a-f]+,/, '').trim(); /** - * 4. Split the text into lines. - * We handle literal newlines and escaped (\n) newlines, - * while filtering out empty lines caused by double-spacing. + * 4. Parse Lines and Paragraphs + * Splits on literal newlines or escaped sequence "\n". + * Filters out empty strings to handle double-spacing in the source. */ const lines = rawText .split(/(?:\r?\n|\\n)+/) @@ -583,14 +582,14 @@ class NovelUpdates implements Plugin.PluginBase { .filter(line => line.length > 0); if (lines.length === 0) { - throw new Error('Chapter content is empty after parsing.'); + throw new Error('Parsed content is empty.'); } - // 5. Extract Title and Content - // The first line is always the title in this payload structure - chapterTitle = lines[0]; + // 5. Assignment + // The first line in the text block is the chapter title. + chapterTitle = `Chapter ${chapterNum}: ${lines[0]}`; - // All subsequent lines are paragraphs + // All remaining lines are the story body. chapterContent = lines .slice(1) .map(p => `

${p}

`) From b447e96e379f8662b1b02572f02532da6eeab2d4 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 7 May 2026 00:02:10 +0200 Subject: [PATCH 10/15] revert versions --- package.json | 2 +- plugins/english/novelupdates.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 85508263c..fd1141596 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnreader-plugins", - "version": "3.0.1", + "version": "3.0.0", "description": "Plugins repo for LNReader", "main": "index.js", "type": "module", diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 20631e12f..3ceb57f0b 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.17'; + version = '0.9.9'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; From a0534f6355b67256505718fa0fdfe21c250e7cfa Mon Sep 17 00:00:00 2001 From: Batorian <53831711+Batorian@users.noreply.github.com> Date: Sat, 9 May 2026 19:06:06 +0200 Subject: [PATCH 11/15] get action hash via regex Co-authored-by: K1ngfish3r <26593485+K1ngfish3r@users.noreply.github.com> --- plugins/english/novelupdates.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 3ceb57f0b..4a3f245a9 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -524,12 +524,20 @@ class NovelUpdates implements Plugin.PluginBase { * 2:T{hexLen},... -> Chapter Body (Title on Line 1, Content below) * 1:{"success":...} -> Series/Chapter JSON metadata */ - const ACTION_HASH = '6047fe8d21566eb56a426de4d5d5ee1eb7e01091f9'; + const html = loadedCheerio('script:contains("script-2")').html(); + if (!html) throw new Error('Failed to find script-2'); + const matches = Array.from(html.matchAll(/"script-2.*?[^_]+([^\\]+)/g)); + const scriptPath = matches[1]?.[1]; + if (!scriptPath) throw new Error('Failed to extract script-2 URL'); + + const scriptUrl = new URL(`/${scriptPath}`, chapterPath).href; + const scriptText = await (await fetchApi(scriptUrl)).text(); + const ACTION_HASH = scriptText.match(/[a-f0-9]{42}/)?.[0]; + if (!ACTION_HASH) throw new Error('Failed to extract ACTION_HASH'); // chapterPath: https://www.mythoriatales.com/series/[slug]/chapter/[num] const urlParts = chapterPath.split('/'); - const slug = urlParts[4]; - const chapterNum = parseInt(urlParts[6], 10); + const [slug, chapterNum] = [urlParts[4], parseInt(urlParts[6], 10)]; const response = await fetchApi(chapterPath, { method: 'POST', From 1d9e865f040a1eddf4b7813439474a0e6312b99d Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 9 May 2026 19:19:53 +0200 Subject: [PATCH 12/15] title extraction from meta --- package.json | 2 +- plugins/english/novelupdates.ts | 66 ++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index fd1141596..85508263c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnreader-plugins", - "version": "3.0.0", + "version": "3.0.1", "description": "Plugins repo for LNReader", "main": "index.js", "type": "module", diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 4a3f245a9..bf9be8a3b 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.9'; + version = '0.9.10'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -513,7 +513,7 @@ class NovelUpdates implements Plugin.PluginBase { chapterContent = loadedCheerio('.entry-content').html()!; break; } - // Last edited in 0.9.9 by Batorian - 06/05/2026 + // Last edited in 0.9.9 by Batorian - 09/05/2026 case 'mythoriatales': { /** * Mythoria Tales uses Next.js Server Actions for chapter delivery. @@ -521,8 +521,9 @@ class NovelUpdates implements Plugin.PluginBase { * * Payload Structure: * 0:{"a":"$@1",...} -> Initialization/Metadata - * 2:T{hexLen},... -> Chapter Body (Title on Line 1, Content below) - * 1:{"success":...} -> Series/Chapter JSON metadata + * 2:T{hexLen},... -> Chapter Body (may span multiple segments) + * 3:T{hexLen},... -> Chapter Body continuation (if split) + * 1:{"success":...} -> Series/Chapter JSON metadata (authoritative title source) */ const html = loadedCheerio('script:contains("script-2")').html(); if (!html) throw new Error('Failed to find script-2'); @@ -557,14 +558,22 @@ class NovelUpdates implements Plugin.PluginBase { /** * 1. Isolate the data segments. - * We split by newline followed by a digit and a type marker (:, {, T). + * We split by newline followed by a digit and a type marker ({, T, E). * Using a lookahead (?=...) ensures the split marker isn't consumed, * allowing us to verify the segment index (e.g., "2:"). */ const segments = rscText.split(/\n(?=\d+:[{TE])/); - // 2. Locate the text segment (usually index 2) - const contentSegment = segments.find(s => s.startsWith('2:T')); + /** + * 2. Locate and join all text content segments. + * Some chapters split their body across multiple T-type segments (e.g., 2:T, 3:T). + * We collect all of them (excluding the 0: init segment) and join into one string, + * stripping each segment's "{index}:T{hexLen}," prefix in the process. + */ + const contentSegment = segments + .filter(s => /^\d+:T/.test(s) && !s.startsWith('0:')) + .map(s => s.replace(/^\d+:T[0-9a-f]+,/, '')) + .join(''); if (!contentSegment) { throw new Error( @@ -573,35 +582,40 @@ class NovelUpdates implements Plugin.PluginBase { } /** - * 3. Extract and Clean Raw Text - * Removes the "2:T{hexLen}," prefix. - * [0-9a-f]+ handles the hexadecimal length provided by the RSC protocol. - */ - const rawText = contentSegment.replace(/^2:T[0-9a-f]+,/, '').trim(); - - /** - * 4. Parse Lines and Paragraphs + * 3. Parse Lines and Paragraphs * Splits on literal newlines or escaped sequence "\n". * Filters out empty strings to handle double-spacing in the source. */ - const lines = rawText + const lines = contentSegment + .trim() .split(/(?:\r?\n|\\n)+/) - .map(line => line.trim()) - .filter(line => line.length > 0); + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0); if (lines.length === 0) { throw new Error('Parsed content is empty.'); } - // 5. Assignment - // The first line in the text block is the chapter title. - chapterTitle = `Chapter ${chapterNum}: ${lines[0]}`; + /** + * 4. Extract title from the "1:{...}" metadata segment. + * This is the authoritative source for the chapter title and number, + * preferred over parsing the first content line. + */ + const metaSegment = segments.find(s => s.startsWith('1:')); + if (metaSegment) { + try { + const meta = JSON.parse(metaSegment.slice(2)); // strip leading "1:" + const title = meta?.data?.chapter?.title; + const num = meta?.data?.chapter?.chapterNumber ?? chapterNum; + if (title) chapterTitle = `Chapter ${num}: ${title}`; + } catch { + // fall back to chapterNum if metadata parsing fails + } + } + if (!chapterTitle) chapterTitle = `Chapter ${chapterNum}`; - // All remaining lines are the story body. - chapterContent = lines - .slice(1) - .map(p => `

${p}

`) - .join('\n'); + // 5. All lines from the content segment are paragraphs. + chapterContent = lines.map((p: string) => `

${p}

`).join('\n'); break; } From 8231f910f886967f9f8de26197a74b4e63added3 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 9 May 2026 19:24:33 +0200 Subject: [PATCH 13/15] fix --- plugins/english/novelupdates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index bf9be8a3b..192183d33 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.10'; + version = '0.9.11'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -554,7 +554,7 @@ class NovelUpdates implements Plugin.PluginBase { throw new Error(`Failed to fetch chapter: ${response.status}`); } - const rscText = await response.text(); + const rscText = (await response.text()).replace(/(\d+:[{TE])/g, '\n$1'); /** * 1. Isolate the data segments. From a3123510e363df29e0b5f663e2eae5fd06727c8a Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 9 May 2026 19:34:22 +0200 Subject: [PATCH 14/15] chapterContent cleanup --- plugins/english/novelupdates.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 192183d33..9c00851b9 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.11'; + version = '0.9.12'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -617,6 +617,16 @@ class NovelUpdates implements Plugin.PluginBase { // 5. All lines from the content segment are paragraphs. chapterContent = lines.map((p: string) => `

${p}

`).join('\n'); + // Clean up custom markup tags: + // Format [dialogue speaker="Name"]text[/dialogue] as "Name: text", drop [sfx] blocks entirely + chapterContent = chapterContent + .replace( + /\[dialogue\s+speaker="([^"]*)"\](.*?)\[\/dialogue\]/gi, + '$1: $2', + ) + .replace(/\[sfx\].*?\[\/sfx\]/gi, '') + .replace(/\[\/?(dialogue|sfx)[^\]]*\]/gi, ''); + break; } // Last edited in 0.9.0 by Batorian - 19/03/2025 From 56872fb75ad76b32c06390ecbcb8f2b5641b52e1 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 9 May 2026 19:37:02 +0200 Subject: [PATCH 15/15] revert versions --- package.json | 2 +- plugins/english/novelupdates.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 85508263c..fd1141596 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnreader-plugins", - "version": "3.0.1", + "version": "3.0.0", "description": "Plugins repo for LNReader", "main": "index.js", "type": "module", diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 9c00851b9..825ec3e7f 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.12'; + version = '0.9.9'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/';