37313f5ffa
* 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.
298 lines
9.0 KiB
TypeScript
298 lines
9.0 KiB
TypeScript
import { load } from 'cheerio';
|
|
import { fetchApi } from '@libs/fetch';
|
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
|
import { Plugin } from '@/types/plugin';
|
|
import { NovelStatus } from '@libs/novelStatus';
|
|
|
|
class IndraTranslations implements Plugin.PluginBase {
|
|
id = 'indratranslations';
|
|
name = 'Indra Translations';
|
|
site = 'https://indratranslations.com';
|
|
version = '1.2.0';
|
|
// icon = 'src/en/indratranslations/icon.png';
|
|
// customCSS = 'src/en/indratranslations/customCSS.css';
|
|
// (optional) Add these files to the repo and uncomment the lines above if you want an icon/custom CSS.
|
|
|
|
// Browser-like headers (important for Cloudflare-y sites)
|
|
private headers = {
|
|
'User-Agent':
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36',
|
|
Referer: this.site,
|
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
'Cache-Control': 'no-cache',
|
|
Pragma: 'no-cache',
|
|
};
|
|
|
|
private async fetchHtml(url: string): Promise<string> {
|
|
const res = await fetchApi(url, { headers: this.headers });
|
|
return await res.text();
|
|
}
|
|
|
|
private absolute(url?: string): string | undefined {
|
|
if (!url) return undefined;
|
|
const u = String(url).trim();
|
|
if (!u) return undefined;
|
|
if (u.startsWith('http')) return u;
|
|
if (u.startsWith('//')) return 'https:' + u;
|
|
if (u.startsWith('/')) return this.site + u;
|
|
return this.site + '/' + u;
|
|
}
|
|
|
|
private clean(text: unknown): string {
|
|
return String(text ?? '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
private chapterNum(name: string): number {
|
|
const m = String(name).match(/(\d+(\.\d+)?)/);
|
|
return m ? Number(m[1]) : 0;
|
|
}
|
|
|
|
/**
|
|
* Indra can render results in different templates.
|
|
* This tries multiple layouts and returns a single unified list.
|
|
*/
|
|
private parseNovelCards($: ReturnType<typeof load>) {
|
|
const out: { name: string; path: string; cover?: string }[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
const push = (name?: string, path?: string, cover?: string) => {
|
|
const cleanName = this.clean(name);
|
|
const cleanPath = String(path || '')
|
|
.replace(this.site, '')
|
|
.trim();
|
|
if (!cleanName || !cleanPath) return;
|
|
if (!cleanPath.includes('/series/')) return;
|
|
|
|
// Normalize trailing slash for consistency
|
|
const normalized = cleanPath.endsWith('/') ? cleanPath : cleanPath + '/';
|
|
if (seen.has(normalized)) return;
|
|
|
|
seen.add(normalized);
|
|
out.push({
|
|
name: cleanName,
|
|
path: normalized,
|
|
cover: cover ? this.absolute(cover) : undefined,
|
|
});
|
|
};
|
|
|
|
// -------- Layout A (Madara-style): .page-item-detail ----------
|
|
$('.page-item-detail').each((_, el) => {
|
|
const a = $(el).find('a[href*="/series/"]').first();
|
|
const href = a.attr('href') || '';
|
|
const title =
|
|
this.clean(a.attr('title')) ||
|
|
this.clean($(el).find('h3 a').text()) ||
|
|
this.clean($(el).find('.post-title a').text());
|
|
|
|
const img =
|
|
$(el).find('img').attr('data-src') ||
|
|
$(el).find('img').attr('data-lazy-src') ||
|
|
$(el).find('img').attr('src');
|
|
|
|
if (href) push(title, href, img || undefined);
|
|
});
|
|
|
|
// -------- Layout B (common search tabs): .c-tabs-item__content ----------
|
|
$('.c-tabs-item__content').each((_, el) => {
|
|
const a =
|
|
$(el).find('a[href*="/series/"]').first() ||
|
|
$(el).find('.tab-thumb a[href*="/series/"]').first();
|
|
|
|
const href = (a as any).attr?.('href') || '';
|
|
const title =
|
|
this.clean($(el).find('.post-title a').text()) ||
|
|
this.clean($(el).find('.tab-summary .post-title a').text()) ||
|
|
this.clean((a as any).attr?.('title')) ||
|
|
this.clean((a as any).text?.());
|
|
|
|
const img =
|
|
$(el).find('img').attr('data-src') ||
|
|
$(el).find('img').attr('data-lazy-src') ||
|
|
$(el).find('img').attr('src');
|
|
|
|
if (href) push(title, href, img || undefined);
|
|
});
|
|
|
|
// -------- Layout C (sometimes search results are in .row or .col wrappers) ----------
|
|
$('.row').each((_, el) => {
|
|
const a = $(el).find('a[href*="/series/"]').first();
|
|
const href = a.attr('href') || '';
|
|
if (!href) return;
|
|
|
|
const title =
|
|
this.clean($(el).find('h3 a').text()) ||
|
|
this.clean($(el).find('.post-title a').text()) ||
|
|
this.clean(a.attr('title')) ||
|
|
this.clean(a.text());
|
|
|
|
const img =
|
|
$(el).find('img').attr('data-src') ||
|
|
$(el).find('img').attr('data-lazy-src') ||
|
|
$(el).find('img').attr('src');
|
|
|
|
push(title, href, img || undefined);
|
|
});
|
|
|
|
// -------- Layout D (fallback: any anchor to /series/) ----------
|
|
// If everything else fails but links exist, still return something.
|
|
if (out.length === 0) {
|
|
$('a[href*="/series/"]').each((_, el) => {
|
|
const a = $(el);
|
|
const href = a.attr('href') || '';
|
|
if (!href) return;
|
|
|
|
const title =
|
|
this.clean(a.attr('title')) || this.clean(a.text()) || 'Unknown';
|
|
|
|
// Try to find an image near the link
|
|
const img =
|
|
a.find('img').attr('data-src') ||
|
|
a.find('img').attr('data-lazy-src') ||
|
|
a.find('img').attr('src') ||
|
|
a.closest('*').find('img').first().attr('data-src') ||
|
|
a.closest('*').find('img').first().attr('data-lazy-src') ||
|
|
a.closest('*').find('img').first().attr('src');
|
|
|
|
push(title, href, img || undefined);
|
|
});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
async popularNovels(pageNo: number) {
|
|
if (pageNo !== 1) return [];
|
|
const html = await this.fetchHtml(`${this.site}/series/`);
|
|
const $ = load(html);
|
|
const parsed = this.parseNovelCards($);
|
|
|
|
return parsed.map(n => ({
|
|
name: n.name,
|
|
path: n.path,
|
|
cover: n.cover,
|
|
}));
|
|
}
|
|
|
|
async searchNovels(searchTerm: string, pageNo: number) {
|
|
if (pageNo !== 1) return [];
|
|
const url = `${this.site}/?s=${encodeURIComponent(searchTerm)}&post_type=wp-manga`;
|
|
const html = await this.fetchHtml(url);
|
|
const $ = load(html);
|
|
return this.parseNovelCards($);
|
|
}
|
|
|
|
async parseNovel(novelPath: string) {
|
|
const url = novelPath.startsWith('http')
|
|
? novelPath
|
|
: this.site + novelPath;
|
|
const html = await this.fetchHtml(url);
|
|
const $ = load(html);
|
|
|
|
const title =
|
|
this.clean($('h1.entry-title').text()) ||
|
|
this.clean($('h1').first().text()) ||
|
|
'Unknown';
|
|
|
|
const cover = this.absolute(
|
|
$('.summary_image img').attr('data-src') ||
|
|
$('.summary_image img').attr('data-lazy-src') ||
|
|
$('.summary_image img').attr('src'),
|
|
);
|
|
|
|
const summary =
|
|
this.clean($('.summary__content').text()) ||
|
|
this.clean($('.description-summary').text()) ||
|
|
undefined;
|
|
|
|
let statusText = '';
|
|
$('.post-content_item').each((_, el) => {
|
|
const label = this.clean(
|
|
$(el).find('.summary-heading').text(),
|
|
).toLowerCase();
|
|
if (label.includes('status')) {
|
|
statusText = this.clean($(el).find('.summary-content').text());
|
|
}
|
|
});
|
|
|
|
const chapters: { name: string; path: string; chapterNumber?: number }[] =
|
|
[];
|
|
|
|
$('li.wp-manga-chapter a').each((_, el) => {
|
|
const href = $(el).attr('href');
|
|
if (!href) return;
|
|
const name = this.clean($(el).text());
|
|
chapters.push({
|
|
name,
|
|
path: href.replace(this.site, ''),
|
|
chapterNumber: this.chapterNum(name),
|
|
});
|
|
});
|
|
|
|
if (chapters.length === 0) {
|
|
$('.wp-manga-chapter a').each((_, el) => {
|
|
const href = $(el).attr('href');
|
|
if (!href) return;
|
|
const name = this.clean($(el).text());
|
|
chapters.push({
|
|
name,
|
|
path: href.replace(this.site, ''),
|
|
chapterNumber: this.chapterNum(name),
|
|
});
|
|
});
|
|
}
|
|
|
|
chapters.sort((a, b) => (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0));
|
|
|
|
const statusLower = String(statusText).toLowerCase();
|
|
const status =
|
|
statusLower.includes('complete') || statusLower.includes('completed')
|
|
? NovelStatus.Completed
|
|
: NovelStatus.Ongoing;
|
|
|
|
return {
|
|
name: title,
|
|
path: novelPath.endsWith('/') ? novelPath : novelPath + '/',
|
|
cover,
|
|
summary,
|
|
status,
|
|
chapters,
|
|
};
|
|
}
|
|
|
|
async parseChapter(chapterPath: string) {
|
|
const url = chapterPath.startsWith('http')
|
|
? chapterPath
|
|
: this.site + chapterPath;
|
|
const html = await this.fetchHtml(url);
|
|
const $ = load(html);
|
|
|
|
const content = $('.reading-content').first().length
|
|
? $('.reading-content').first()
|
|
: $('.text-left').first().length
|
|
? $('.text-left').first()
|
|
: $('.entry-content').first();
|
|
|
|
if (!content.length) {
|
|
return `\nUnable to load chapter content.\n\n`;
|
|
}
|
|
|
|
content.find('script, style, ins, iframe, noscript').remove();
|
|
|
|
return content.html() ?? '';
|
|
}
|
|
|
|
filters: Filters = {
|
|
sort: {
|
|
label: 'Sort',
|
|
value: 'Latest',
|
|
options: [{ label: 'Latest', value: 'Latest' }],
|
|
type: FilterTypes.Picker,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default new IndraTranslations();
|