fix: Handle Fenrir Realm API Change (#2086)
* Update fenrirrealm.ts
* Update fenrirrealm.ts
* Update fenrirrealm.ts
* Improve novel status extraction and clean summary
Enhanced status parsing logic and cleaned up summary HTML.
* Enhance parseNovelFromApi for better data handling
Update parseNovelFromApi to clean HTML tags and set default status.
* feat: add Fenrir Realm plugin for novel fetching and parsing.
* feat: add Fenrir Realm plugin for novel fetching and parsing.
* feat: add Fenrir Realm plugin for fetching and parsing novels and chapters.
* feat: add Fenrir Realm plugin for novel browsing and reading
* feat: Add Fenrir Realm plugin for fetching and parsing novels and chapters.
* feat: add Fenrir Realm plugin for novel fetching and parsing.
* feat: Add Fenrir Realm plugin for fetching and parsing novels.
* feat: add Fenrir Realm plugin
* feat: add Fenrir Realm plugin
* Update auto-generated plugins index
* chore: delete file.
* feat: Add new Fenrir Realm plugin.
* feat: Add Fenrir Realm plugin and establish a new centralized plugin index.
* Delete a file.
* Fix Fenrir Realm plugin fallback slug for old library entries
* Enhance fenrirrealm plugin fallback to auto-heal renamed slugs using search API
* feat: Add Fenrir Realm plugin, Gunslinger novel data, and a related HTML parsing test script.
* Implement fuzzy search backoff to handle misspelled user queries in searchNovels
* Refactor Fenrir Realm plugin to fetch novel details directly from the undocumented JSON API, bypassing HTML scraping and Cloudflare rate-limiting issues
* Fix fenrirrealm plugin falling back to clean slug and migrating parseNovel to JSON API
* Fix fetching empty chapters by adopting exact novel slug formatting dictated by the fenrir realm JSON chapter payload instead of statically building chapter-{{id}}
* Auto-heal legacy broken chapter URLs in user libraries by dynamically resolving slugs via API fallback during parseChapter
* Enhance parseChapter auto-heal with exhaustive search-fallback to natively resolve heavily outdated overarching novel slugs
* Optimize search fallback heuristic to prevent Cloudflare 403 network request rate limits
* Intercept generic fetchApi crashes caused by Cloudflare 403 blocks with localized WebView Captcha-prompt error responses
* feat: add new gunslinger and fenrirrealm plugins, along with associated test and utility files.
* feat: Add Fenrir Realm plugin with associated test and support files.
* feat: Add Fenrir Realm plugin with associated test files and data.
* Distribute explicitly defined anti-Cloudflare fetch Headers globally to all internal APIs throughout the legacy auto-heal fallback routing
* Silence HTTP 500 SvelteKit crash exceptions in parseChapter to allow robust legacy URL auto-healing to trigger
* Add inline catch guards to ALL async calls in parseChapter auto-heal to prevent React Native fetch exceptions from aborting legacy URL recovery
* feat: Add Fenrir Realm plugin for 'Surviving as a Gunslinger in Wild West' novel, including associated data and test files.
* Refactor chapter parsing logic to improve reliability and handle potential changes in website structure.
* Refactor chapter parsing logic to improve error handling and fallback mechanism, utilizing .content-area class for HTML extraction.
* Update version number to 1.0.13 in FenrirRealmPlugin
* Refactor Fenrir Realm plugin for improved API calls
* Update Fenrir Realm version to 1.0.13
* feat(fenrir): rewrite parseChapter with sveltekit fallback and path healing
* feat(fenrir): robust parseChapter with prosemirror json fallback and path healing
* feat(fenrir): restore to official commit ef55446 (v1.0.16) [no-verify]
* feat: Add Fenrir Realm plugin and settings component, and remove various configuration files.
* feat: Introduce settings management UI and API, establish a plugin system, and remove obsolete configuration files.
* feat: Introduce a new plugin indexing system, add Fenrir Realm plugin and settings component, and remove various configuration files.
* feat: Add Fenrir Realm plugin and settings component, remove old configuration files, and update proxy.
* feat: Introduce a settings UI and plugin system, removing old configuration files and updating the proxy.
* Add missing file
Add missing file
* Add plugins index file with multiple language imports
This file imports various plugins from different languages and creates a PLUGINS array that exports them for use in the application.
* Delete index.ts
* feat: add Indra Translations and NovelRest plugins.
This commit is contained in:
+242
-229
@@ -1,229 +1,242 @@
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
/**
|
||||
* NovelRest LNReader Plugin
|
||||
*
|
||||
* This plugin allows LNReader to fetch novels from NovelRest (novelrest.vercel.app)
|
||||
*
|
||||
* Features:
|
||||
* - Browse popular novels with pagination
|
||||
* - Search novels
|
||||
* - Read chapters
|
||||
* - Filter by status
|
||||
* - Sort by latest/popular/rating/updated
|
||||
*/
|
||||
class NovelRestPlugin implements Plugin.PluginBase {
|
||||
id = 'novelrest';
|
||||
name = 'NovelRest';
|
||||
icon = 'src/en/novelrest/icon.png';
|
||||
site = 'https://novelrest.vercel.app';
|
||||
apiBase = 'https://novelrest.vercel.app/api/lnreader';
|
||||
version = '1.0.0';
|
||||
|
||||
filters: Filters = {
|
||||
status: {
|
||||
type: FilterTypes.Picker,
|
||||
label: 'Status',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Ongoing', value: 'ONGOING' },
|
||||
{ label: 'Completed', value: 'COMPLETED' },
|
||||
{ label: 'Hiatus', value: 'HIATUS' },
|
||||
],
|
||||
},
|
||||
sort: {
|
||||
type: FilterTypes.Picker,
|
||||
label: 'Sort By',
|
||||
value: 'latest',
|
||||
options: [
|
||||
{ label: 'Latest', value: 'latest' },
|
||||
{ label: 'Popular', value: 'popular' },
|
||||
{ label: 'Rating', value: 'rating' },
|
||||
{ label: 'Updated', value: 'updated' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch popular/latest novels with pagination
|
||||
*/
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', pageNo.toString());
|
||||
params.set('limit', '20');
|
||||
|
||||
if (filters?.status?.value) {
|
||||
params.set('status', filters.status.value as string);
|
||||
}
|
||||
|
||||
const sortBy = showLatestNovels ? 'latest' : (filters?.sort?.value as string || 'popular');
|
||||
params.set('sort', sortBy);
|
||||
|
||||
try {
|
||||
const url = `${this.apiBase}/novels?${params.toString()}`;
|
||||
const response = await fetchApi(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.novels && Array.isArray(data.novels)) {
|
||||
for (const novel of data.novels) {
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
path: novel.slug, // Just the slug, we'll build full path in parseNovel
|
||||
cover: novel.coverImage || defaultCover,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error fetching popular novels:', error);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse novel details and chapter list
|
||||
*/
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: 'Untitled',
|
||||
};
|
||||
|
||||
try {
|
||||
// novelPath is just the slug
|
||||
const slug = novelPath.replace(/^\/novels\//, ''); // Clean up if path prefix exists
|
||||
|
||||
const response = await fetchApi(`${this.apiBase}/novels/${slug}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data) {
|
||||
novel.name = data.title || 'Untitled';
|
||||
novel.author = data.author || '';
|
||||
novel.cover = data.coverImage || defaultCover;
|
||||
novel.genres = Array.isArray(data.genres)
|
||||
? data.genres.map((g: { name: string } | string) => typeof g === 'string' ? g : g.name).join(', ')
|
||||
: '';
|
||||
novel.summary = data.description || '';
|
||||
|
||||
// Map status
|
||||
switch (data.status) {
|
||||
case 'COMPLETED':
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
case 'ONGOING':
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case 'HIATUS':
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
|
||||
// Parse chapters
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
if (data.chapters && Array.isArray(data.chapters)) {
|
||||
for (const chapter of data.chapters) {
|
||||
chapters.push({
|
||||
name: chapter.title || `Chapter ${chapter.number}`,
|
||||
path: `${slug}/${chapter.number}`, // slug/chapterNumber format
|
||||
releaseTime: chapter.createdAt || undefined,
|
||||
chapterNumber: chapter.number,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error parsing novel:', error);
|
||||
}
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse chapter content
|
||||
*/
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
try {
|
||||
// chapterPath format: "slug/chapterNumber"
|
||||
const parts = chapterPath.split('/');
|
||||
const chapterNumber = parts.pop();
|
||||
const slug = parts.join('/');
|
||||
|
||||
const response = await fetchApi(`${this.apiBase}/novels/${slug}/chapters/${chapterNumber}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.contentHtml) {
|
||||
return data.contentHtml;
|
||||
}
|
||||
|
||||
return '<p>Chapter content could not be loaded.</p>';
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error parsing chapter:', error);
|
||||
return '<p>Error loading chapter content.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search novels by term
|
||||
*/
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('q', searchTerm);
|
||||
params.set('page', pageNo.toString());
|
||||
params.set('limit', '20');
|
||||
|
||||
const response = await fetchApi(`${this.apiBase}/novels?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.novels && Array.isArray(data.novels)) {
|
||||
for (const novel of data.novels) {
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
path: novel.slug,
|
||||
cover: novel.coverImage || defaultCover,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error searching novels:', error);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve full URL for novel or chapter
|
||||
*/
|
||||
resolveUrl = (path: string, isNovel?: boolean): string => {
|
||||
if (isNovel) {
|
||||
return `${this.site}/novels/${path}`;
|
||||
}
|
||||
// For chapters, path is "slug/chapterNumber"
|
||||
return `${this.site}/novels/${path}`;
|
||||
};
|
||||
}
|
||||
|
||||
export default new NovelRestPlugin();
|
||||
|
||||
// trigger build
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { defaultCover } from '@libs/defaultCover';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
/**
|
||||
* NovelRest LNReader Plugin
|
||||
*
|
||||
* This plugin allows LNReader to fetch novels from NovelRest (novelrest.vercel.app)
|
||||
*
|
||||
* Features:
|
||||
* - Browse popular novels with pagination
|
||||
* - Search novels
|
||||
* - Read chapters
|
||||
* - Filter by status
|
||||
* - Sort by latest/popular/rating/updated
|
||||
*/
|
||||
class NovelRestPlugin implements Plugin.PluginBase {
|
||||
id = 'novelrest';
|
||||
name = 'NovelRest';
|
||||
icon = 'src/en/novelrest/icon.png';
|
||||
site = 'https://novelrest.vercel.app';
|
||||
apiBase = 'https://novelrest.vercel.app/api/lnreader';
|
||||
version = '1.0.0';
|
||||
|
||||
filters: Filters = {
|
||||
status: {
|
||||
type: FilterTypes.Picker,
|
||||
label: 'Status',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Ongoing', value: 'ONGOING' },
|
||||
{ label: 'Completed', value: 'COMPLETED' },
|
||||
{ label: 'Hiatus', value: 'HIATUS' },
|
||||
],
|
||||
},
|
||||
sort: {
|
||||
type: FilterTypes.Picker,
|
||||
label: 'Sort By',
|
||||
value: 'latest',
|
||||
options: [
|
||||
{ label: 'Latest', value: 'latest' },
|
||||
{ label: 'Popular', value: 'popular' },
|
||||
{ label: 'Rating', value: 'rating' },
|
||||
{ label: 'Updated', value: 'updated' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch popular/latest novels with pagination
|
||||
*/
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', pageNo.toString());
|
||||
params.set('limit', '20');
|
||||
|
||||
if (filters?.status?.value) {
|
||||
params.set('status', filters.status.value as string);
|
||||
}
|
||||
|
||||
const sortBy = showLatestNovels
|
||||
? 'latest'
|
||||
: (filters?.sort?.value as string) || 'popular';
|
||||
params.set('sort', sortBy);
|
||||
|
||||
try {
|
||||
const url = `${this.apiBase}/novels?${params.toString()}`;
|
||||
const response = await fetchApi(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.novels && Array.isArray(data.novels)) {
|
||||
for (const novel of data.novels) {
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
path: novel.slug, // Just the slug, we'll build full path in parseNovel
|
||||
cover: novel.coverImage || defaultCover,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error fetching popular novels:', error);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse novel details and chapter list
|
||||
*/
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: 'Untitled',
|
||||
};
|
||||
|
||||
try {
|
||||
// novelPath is just the slug
|
||||
const slug = novelPath.replace(/^\/novels\//, ''); // Clean up if path prefix exists
|
||||
|
||||
const response = await fetchApi(`${this.apiBase}/novels/${slug}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data) {
|
||||
novel.name = data.title || 'Untitled';
|
||||
novel.author = data.author || '';
|
||||
novel.cover = data.coverImage || defaultCover;
|
||||
novel.genres = Array.isArray(data.genres)
|
||||
? data.genres
|
||||
.map((g: { name: string } | string) =>
|
||||
typeof g === 'string' ? g : g.name,
|
||||
)
|
||||
.join(', ')
|
||||
: '';
|
||||
novel.summary = data.description || '';
|
||||
|
||||
// Map status
|
||||
switch (data.status) {
|
||||
case 'COMPLETED':
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
case 'ONGOING':
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case 'HIATUS':
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
|
||||
// Parse chapters
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
if (data.chapters && Array.isArray(data.chapters)) {
|
||||
for (const chapter of data.chapters) {
|
||||
chapters.push({
|
||||
name: chapter.title || `Chapter ${chapter.number}`,
|
||||
path: `${slug}/${chapter.number}`, // slug/chapterNumber format
|
||||
releaseTime: chapter.createdAt || undefined,
|
||||
chapterNumber: chapter.number,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error parsing novel:', error);
|
||||
}
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse chapter content
|
||||
*/
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
try {
|
||||
// chapterPath format: "slug/chapterNumber"
|
||||
const parts = chapterPath.split('/');
|
||||
const chapterNumber = parts.pop();
|
||||
const slug = parts.join('/');
|
||||
|
||||
const response = await fetchApi(
|
||||
`${this.apiBase}/novels/${slug}/chapters/${chapterNumber}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.contentHtml) {
|
||||
return data.contentHtml;
|
||||
}
|
||||
|
||||
return '<p>Chapter content could not be loaded.</p>';
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error parsing chapter:', error);
|
||||
return '<p>Error loading chapter content.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search novels by term
|
||||
*/
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('q', searchTerm);
|
||||
params.set('page', pageNo.toString());
|
||||
params.set('limit', '20');
|
||||
|
||||
const response = await fetchApi(
|
||||
`${this.apiBase}/novels?${params.toString()}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.novels && Array.isArray(data.novels)) {
|
||||
for (const novel of data.novels) {
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
path: novel.slug,
|
||||
cover: novel.coverImage || defaultCover,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NovelRest: Error searching novels:', error);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve full URL for novel or chapter
|
||||
*/
|
||||
resolveUrl = (path: string, isNovel?: boolean): string => {
|
||||
if (isNovel) {
|
||||
return `${this.site}/novels/${path}`;
|
||||
}
|
||||
// For chapters, path is "slug/chapterNumber"
|
||||
return `${this.site}/novels/${path}`;
|
||||
};
|
||||
}
|
||||
|
||||
export default new NovelRestPlugin();
|
||||
|
||||
// trigger build
|
||||
|
||||
Reference in New Issue
Block a user