Multisrc Readnovelfull: add novelfull (#1657)

* add novelfull

* delete novelfull
This commit is contained in:
K1ngfish3r
2025-04-28 07:21:23 +05:00
committed by GitHub
parent dfb2a01e7a
commit 177d8eb836
7 changed files with 334 additions and 338 deletions

Before

Width:  |  Height:  |  Size: 758 B

After

Width:  |  Height:  |  Size: 758 B

+17 -6
View File
@@ -4,7 +4,7 @@
### This generator is brought over from the madara generator one folder away ### This generator is brought over from the madara generator one folder away
it should(!) work for all sites that uses this theme it should(!) work for all sites that uses this theme. If not, edit.
## Add a new source ## Add a new source
@@ -16,12 +16,23 @@ To add a new source you need to add it to sources.json:
- sourceName: the name of the source (you can use the value of "name" in "https://site.com/wp-json/" if it exists) - sourceName: the name of the source (you can use the value of "name" in "https://site.com/wp-json/" if it exists)
- sourceSite: the site url - sourceSite: the site url
- options: the options of the source - options: the options of the source
- lang: the language of the source (default: "English") (check that the language - lang?: the language of the source (default: "English") (check that the language
exists in the languages (check folder names in "plugins/")) exists in the languages (check folder names in "plugins/"))
- versionIncrements: needs to be updated everytime the site url is updated - versionIncrements?: needs to be updated everytime the site url is updated
- popularPage: the href value using this CSS selector `a[title="Latest Release"]` - latestPage: the href value using this CSS selector `a[title="Latest Release"]`
- ajax-chapter-list: if chapters are obtained from an ajax endpoint - searchPage: path for search
- pageParam: if a site uses something other than page for accessing pages - chapterListing?: if chapters are obtained from an ajax endpoint different from what is default.
- chapterParam?: same with chapterListing, but its the key for the form body. default: novelId
- pageParam?: if a site uses something other than "page" for accessing pages
// All the following is for when if a page uses a main path for querying novels and just adds params to it to get different outputs (see urls in lightnovelplus)
- novelListing?: Main path for browsing novels
- typeParam?: Field for entering the type of novelListing (eg: latest, hot, completed, etc), default: type
- genreParam?: Field for entering type of novelListing dedicated for genres (since param websites dont use the path change), default: category_novel
- genreKey?: Field for entering key representing the genre within the genreParam, default: id (eg: ?type=category_novel&id={value})
- langParam?: Field for entering the key representing language(?), do not understand reason for this param honestly, but recreate normal website behaviour as much as possible, default: none.
- urlLangCode?: the value for langParam, default: none. eg: 'en'
### filters ### filters
@@ -0,0 +1,174 @@
{
"filters": {
"type": {
"type": "Picker",
"label": "Novel Listing",
"value": "most-popular",
"options": [
{
"label": "Hot Novel",
"value": "hot-novel"
},
{
"label": "Completed Novel",
"value": "completed-novel"
},
{
"label": "Most Popular",
"value": "most-popular"
}
]
},
"genres": {
"type": "Picker",
"label": "Genre",
"value": "",
"options": [
{
"label": "Shounen",
"value": "genre/Shounen"
},
{
"label": "Harem",
"value": "genre/Harem"
},
{
"label": "Comedy",
"value": "genre/Comedy"
},
{
"label": "Martial Arts",
"value": "genre/Martial+Arts"
},
{
"label": "School Life",
"value": "genre/School+Life"
},
{
"label": "Mystery",
"value": "genre/Mystery"
},
{
"label": "Shoujo",
"value": "genre/Shoujo"
},
{
"label": "Romance",
"value": "genre/Romance"
},
{
"label": "Sci-fi",
"value": "genre/Sci-fi"
},
{
"label": "Gender Bender",
"value": "genre/Gender+Bender"
},
{
"label": "Mature",
"value": "genre/Mature"
},
{
"label": "Fantasy",
"value": "genre/Fantasy"
},
{
"label": "Horror",
"value": "genre/Horror"
},
{
"label": "Drama",
"value": "genre/Drama"
},
{
"label": "Tragedy",
"value": "genre/Tragedy"
},
{
"label": "Supernatural",
"value": "genre/Supernatural"
},
{
"label": "Ecchi",
"value": "genre/Ecchi"
},
{
"label": "Xuanhuan",
"value": "genre/Xuanhuan"
},
{
"label": "Adventure",
"value": "genre/Adventure"
},
{
"label": "Action",
"value": "genre/Action"
},
{
"label": "Psychological",
"value": "genre/Psychological"
},
{
"label": "Xianxia",
"value": "genre/Xianxia"
},
{
"label": "Wuxia",
"value": "genre/Wuxia"
},
{
"label": "Historical",
"value": "genre/Historical"
},
{
"label": "Slice of Life",
"value": "genre/Slice+of+Life"
},
{
"label": "Seinen",
"value": "genre/Seinen"
},
{
"label": "Lolicon",
"value": "genre/Lolicon"
},
{
"label": "Adult",
"value": "genre/Adult"
},
{
"label": "Josei",
"value": "genre/Josei"
},
{
"label": "Sports",
"value": "genre/Sports"
},
{
"label": "Smut",
"value": "genre/Smut"
},
{
"label": "Mecha",
"value": "genre/Mecha"
},
{
"label": "Yaoi",
"value": "genre/Yaoi"
},
{
"label": "Shounen Ai",
"value": "genre/Shounen+Ai"
},
{
"label": "History",
"value": "genre/History"
},
{
"label": "Martial",
"value": "genre/Martial"
}
]
}
}
}
+106 -97
View File
@@ -8,6 +8,26 @@ import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
function extractValueFromHref(href, baseUrl, name, isGenre = false) {
const fullUrl = new URL(decodeURI(href), baseUrl);
const params = fullUrl.searchParams;
if (isGenre && params.get('type') === 'category_novel' && params.has('id')) {
return params.get('id');
}
const value = params.has('type')
? params.get('type')
: fullUrl.pathname.substring(1) || '';
if (!value) {
console.warn(
`Skipping option for ${name}: Could not determine value from href "${href}"`,
);
}
return value;
}
function getFilters(name, html, baseUrl) { function getFilters(name, html, baseUrl) {
const $ = cheerio.load(html); const $ = cheerio.load(html);
const filters = { const filters = {
@@ -40,30 +60,13 @@ function getFilters(name, html, baseUrl) {
if (!href || !label || title === 'Latest Release') return; if (!href || !label || title === 'Latest Release') return;
try { const value = extractValueFromHref(href, baseUrl, name);
const fullUrl = new URL(decodeURI(href), baseUrl);
const params = fullUrl.searchParams;
const value = params.has('type') if (value) {
? params.get('type') typeOptions.push({ label, value });
: (fullUrl.pathname.startsWith('/') if (title === 'Most Popular') {
? fullUrl.pathname.substring(1) defaultValue = value;
: fullUrl.pathname) || '';
if (value) {
typeOptions.push({ label, value });
if (title === 'Most Popular') {
defaultValue = value;
}
} else {
console.warn(
`Skipping type option "${label}" for ${name}: Could not determine value from href "${href}"`,
);
} }
} catch (error) {
console.error(
`Error processing type URL (${href}) for ${name}: ${error.message}`,
);
} }
}); });
filters.filters.type.options = typeOptions; filters.filters.type.options = typeOptions;
@@ -78,36 +81,18 @@ function getFilters(name, html, baseUrl) {
if (!href || !label) return; if (!href || !label) return;
try { const value = extractValueFromHref(href, baseUrl, name, true);
const fullUrl = new URL(decodeURI(href), baseUrl);
const params = fullUrl.searchParams;
const value = if (value) {
params.get('type') === 'category_novel' && params.has('id') genreOptions.push({ label, value });
? params.get('id')
: (fullUrl.pathname.startsWith('/')
? fullUrl.pathname.substring(1)
: fullUrl.pathname) || '';
if (value) {
genreOptions.push({ label, value });
} else {
console.warn(
`Skipping genre option "${label}" for ${name}: Could not determine value from href "${href}"`,
);
}
} catch (error) {
console.error(
`Error processing genre URL (${href}) for ${name}: ${error.message}`,
);
} }
}); });
filters.filters.genre.options = genreOptions; // Genre has no default filters.filters.genres.options = genreOptions; // Genre has no default
// --- Validation and Saving --- // --- Validation and Saving ---
if ( if (
filters.filters.type.options.length === 0 || filters.filters.type.options.length === 0 ||
filters.filters.genre.options.length === 0 filters.filters.genres.options.length === 0
) { ) {
console.warn( console.warn(
`🚨Warning for ${name}: Type or Genre options might be incomplete or empty. (${path.join(__dirname, 'filters', name + '.json')})🚨`, `🚨Warning for ${name}: Type or Genre options might be incomplete or empty. (${path.join(__dirname, 'filters', name + '.json')})🚨`,
@@ -144,73 +129,97 @@ async function getFiltersFromURL(name, url) {
} }
} }
async function askGetFilter() { const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K';
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
}); });
const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; async function askGetFilter() {
rl.question( try {
'Enter the id of the site (same one as in sources.json): ', const sources = JSON.parse(
async name => { fs.readFileSync(path.join(__dirname, 'sources.json'), 'utf-8'),
rl.question( );
EREASE_PREV_LINE +
"Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ", rl.question(
async method => { 'Enter the id of the site (same one as in sources.json): ',
if (method.toLowerCase() === 'url') { async name => {
const sources = JSON.parse( const baseUrl = await getBaseUrl(name, sources);
fs.readFileSync(path.join(__dirname, 'sources.json'), 'utf-8'),
); rl.question(
const source = sources.find(s => s.id === name); EREASE_PREV_LINE +
if (source && source.sourceSite) { "Do you want to get the filters from a URL or the html text? (if url doesn't work try html) (url/html): ",
console.log('Getting filters from', source.sourceSite); async method => {
try { if (method.toLowerCase() === 'url') {
await getFiltersFromURL(name, source.sourceSite); if (baseUrl) {
} catch (e) { console.log('Getting filters from', baseUrl);
try {
await getFiltersFromURL(name, baseUrl);
} catch (e) {
console.error('Error while getting filters from', baseUrl);
console.log(e.message || e);
}
} else {
console.error( console.error(
'Error while getting filters from', 'Cannot get filters from URL: Base URL is not available.',
source.sourceSite,
); );
console.log(e.message || e);
} }
rl.close(); rl.close();
} else { } else {
rl.question( rl.question(
EREASE_PREV_LINE + EREASE_PREV_LINE + 'Enter the absolute path to the HTML file: ',
'Enter the URL (same one as in sources.json): ', async filePath => {
async url => {
rl.close();
try { try {
await getFiltersFromURL(name, url); const html = fs.readFileSync(filePath, 'utf-8');
if (baseUrl) {
console.log('Using base URL:', baseUrl);
} else {
console.error(
'Cannot get filters from HTML: Base URL is not available.',
);
}
getFilters(name, html, baseUrl);
} catch (e) { } catch (e) {
console.error('Error while getting filters from', url); console.error(
console.log(e.message || e); 'Error reading HTML file or getting filters:',
e.message || e,
);
} finally {
rl.close();
} }
}, },
); );
} }
} else { },
process.stdout.write( );
EREASE_PREV_LINE + },
`Enter the html text from the page at {sourceSite}/?s=&post_type=wp-manga (at the end press ENTER then press CTRL+C): `, );
); } catch (e) {
let html = ''; console.error('Error reading sources.json:', e.message || e);
rl.on('SIGINT', () => { rl.close();
console.log('Stopeed reading input, creating filters file'); }
getFilters(name, html, url);
rl.close();
});
rl.on('line', line => {
html += line + '\n';
});
}
},
);
},
);
} }
askGetFilter(); askGetFilter();
async function getBaseUrl(name, sources) {
const source = sources.find(s => s.id === name);
if (source && source.sourceSite) {
console.log('Using base URL from sources.json:', source.sourceSite);
return source.sourceSite;
} else {
console.warn(
`Source with id "${name}" not found or missing sourceSite in sources.json.`,
);
return new Promise(resolve => {
rl.question(
EREASE_PREV_LINE + 'Enter the base URL for the site: ',
manualBaseUrl => {
resolve(manualBaseUrl);
},
);
});
}
}
export { getFiltersFromURL }; export { getFiltersFromURL };
+13 -4
View File
@@ -24,14 +24,13 @@
"sourceName": "LightNovelPlus", "sourceName": "LightNovelPlus",
"options": { "options": {
"novelListing": "book/bookclass.html", "novelListing": "book/bookclass.html",
"chapterListing": "get_chapter_list",
"searchPage": "book/search.html", "searchPage": "book/search.html",
"chapterListing": "get_chapter_list",
"chapterParam": "bookId",
"latestPage": "last_release", "latestPage": "last_release",
"genreParam": "category_novel",
"pageParam": "page_num", "pageParam": "page_num",
"langParam": "language", "langParam": "language",
"urlLangCode": "en", "urlLangCode": "en"
"ajaxChapterParam": "bookId"
} }
}, },
{ {
@@ -42,5 +41,15 @@
"latestPage": "sort/nov-love-daily-update", "latestPage": "sort/nov-love-daily-update",
"searchPage": "search" "searchPage": "search"
} }
},
{
"id": "novelfull",
"sourceSite": "https://novelfull.com/",
"sourceName": "NovelFull",
"options": {
"latestPage": "latest-release-novel",
"searchPage": "search",
"chapterListing": "ajax-chapter-option"
}
} }
] ]
+24 -18
View File
@@ -9,15 +9,15 @@ type ReadNovelFullOptions = {
versionIncrements?: number; versionIncrements?: number;
latestPage: string; latestPage: string;
searchPage: string; searchPage: string;
novelListing?: string;
chapterListing?: string; chapterListing?: string;
TypeParam?: string; chapterParam?: string;
pageParam?: string;
novelListing?: string;
typeParam?: string;
genreParam?: string; genreParam?: string;
genreKey?: string; genreKey?: string;
pageParam?: string;
langParam?: string; langParam?: string;
urlLangCode?: string; urlLangCode?: string;
ajaxChapterParam?: string;
}; };
export type ReadNovelFullMetadata = { export type ReadNovelFullMetadata = {
@@ -43,7 +43,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
this.icon = `multisrc/readnovelfull/${metadata.id.toLowerCase()}/icon.png`; this.icon = `multisrc/readnovelfull/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
const versionIncrements = metadata.options?.versionIncrements || 0; const versionIncrements = metadata.options?.versionIncrements || 0;
this.version = `2.0.${0 + versionIncrements}`; this.version = `2.0.${1 + versionIncrements}`;
this.options = metadata.options; this.options = metadata.options;
this.filters = metadata.filters; this.filters = metadata.filters;
} }
@@ -62,7 +62,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
parseNovels($: CheerioAPI): Plugin.NovelItem[] { parseNovels($: CheerioAPI): Plugin.NovelItem[] {
const baseUrl = this.site; const baseUrl = this.site;
const novels = $('.list-novel .row, .list-truyen .row') const novels = $('.archive .list-novel .row, .archive .list-truyen .row')
.toArray() .toArray()
.map(el => { .map(el => {
const $el = $(el); const $el = $(el);
@@ -70,18 +70,18 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
const novelName = $el.find('h3').text()?.trim(); const novelName = $el.find('h3').text()?.trim();
const rawHref = $el.find('a').attr('href'); const rawHref = $el.find('a').attr('href');
const rawCover = const rawCover =
$el.find('img').attr('data-src') || $el.find('img').attr('src'); $el.find('img').attr('src') ??
$el.find('img').attr('data-cfsrc') ??
$el.find('img').attr('data-src');
if (!novelName || !rawHref) { if (!novelName || !rawHref) {
return null; return null;
} }
let novelPath: string;
let novelCover: string | undefined;
const novelUrlObject = new URL(rawHref, baseUrl); const novelUrlObject = new URL(rawHref, baseUrl);
novelPath = novelUrlObject.pathname.slice(1); const novelPath = novelUrlObject.pathname.slice(1);
let novelCover: string | undefined;
if (rawCover) { if (rawCover) {
novelCover = new URL(rawCover, baseUrl).href; novelCover = new URL(rawCover, baseUrl).href;
} }
@@ -107,12 +107,12 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
if (this.options.novelListing) { if (this.options.novelListing) {
// URL structure with parameters // URL structure with parameters
const params = new URLSearchParams(); const params = new URLSearchParams();
const typeParam = this.options.TypeParam || 'type'; // Default to 'type' if not specified const typeParam = this.options.typeParam || 'type'; // Default to 'type' if not specified
if (showLatestNovels) { if (showLatestNovels) {
params.append(typeParam, this.options.latestPage); params.append(typeParam, this.options.latestPage);
} else if (filters.genres.value.length) { } else if (filters.genres.value.length) {
params.append(typeParam, this.options.genreParam!); params.append(typeParam, this.options.genreParam || 'category_novel'); // same below
const genreKey = this.options.genreKey || 'id'; // Default to 'id' if not specified const genreKey = this.options.genreKey || 'id'; // Default to 'id' if not specified
params.append(genreKey, filters.genres.value); params.append(genreKey, filters.genres.value);
} else { } else {
@@ -136,7 +136,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
if (showLatestNovels) { if (showLatestNovels) {
basePage = this.options.latestPage; basePage = this.options.latestPage;
} else if (filters?.genres?.value?.length) { } else if (filters.genres.value.length) {
basePage = filters.genres.value; basePage = filters.genres.value;
} else { } else {
basePage = filters.type.value; basePage = filters.type.value;
@@ -159,7 +159,12 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
novel.name = $('h3.title').first().text().trim(); novel.name = $('h3.title').first().text().trim();
const coverUrl = const coverUrl =
$('.book img').attr('data-src') || $('.book img').attr('src'); $('.book img').attr('src') ??
$('.book img').attr('data-cfsrc') ??
$('.book img').attr('data-src') ??
$('.book noscript img').attr('src') ??
$('meta[name="image"]').attr('content');
if (coverUrl) { if (coverUrl) {
novel.cover = new URL(coverUrl, baseUrl).href; novel.cover = new URL(coverUrl, baseUrl).href;
} }
@@ -206,13 +211,14 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
const novelId = dataNovelId || (idMatch ? idMatch[0] : null); const novelId = dataNovelId || (idMatch ? idMatch[0] : null);
if (novelId) { if (novelId) {
const ajaxParam = this.options.ajaxChapterParam || 'novelId';
const chapterListing = const chapterListing =
this.options.chapterListing || 'ajax/chapter-archive'; this.options.chapterListing || 'ajax/chapter-archive';
const ajaxParam = this.options.chapterParam || 'novelId';
const params = new URLSearchParams({ const params = new URLSearchParams({
[ajaxParam]: novelId.toString(), [ajaxParam]: novelId.toString(),
}); });
const chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`; const chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`;
$ = await this.getCheerio(chaptersUrl, false); $ = await this.getCheerio(chaptersUrl, false);
@@ -241,8 +247,8 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
const $ = await this.getCheerio(this.site + chapterPath, false); const $ = await this.getCheerio(this.site + chapterPath, false);
$('div.ads, div.unlock-buttons').remove();
return $('#chr-content, #chapter-content').html()!; return $('#chr-content, #chapter-content').html()! || '';
} }
async searchNovels( async searchNovels(
-213
View File
@@ -1,213 +0,0 @@
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
class NovelFull implements Plugin.PluginBase {
id = 'novelfull';
name = 'NovelFull';
version = '1.0.3';
icon = 'src/en/novelfull/icon.png';
site = 'https://novelfull.com/';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.col-truyen-main .list-truyen .row').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text();
// Images from the site are now lazy loaded, so we need to check data-cfsrc as well
let novelCover =
loadedCheerio(ele).find('img').attr('src') ??
loadedCheerio(ele).find('img').attr('data-cfsrc');
novelCover = novelCover ? this.site + novelCover.slice(1) : undefined;
const novelUrl = loadedCheerio(ele)
.find('h3.truyen-title > a')
.attr('href')
?.slice(1);
if (!novelUrl) return;
const novel = {
name: novelName,
cover: novelCover,
path: novelUrl,
};
novels.push(novel);
});
return novels;
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '') link += `genre/${filters.genre.value}`;
else link += filters.sort.value;
link += `?page=${page}`;
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
let loadedCheerio = parseHTML(body);
// Images from the site are now lazy loaded, so we need to check data-cfsrc as well (and fallback to other locations)
let cover =
loadedCheerio('div.book > img').attr('src') ??
loadedCheerio('div.book > img').attr('data-cfsrc') ??
loadedCheerio('div.book > noscript > img').attr('src') ??
loadedCheerio('meta[name="image"]').attr('content');
cover = cover ? this.site + cover.slice(1) : undefined;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('div.book > img').attr('alt') || 'Untitled',
cover: cover,
summary: loadedCheerio('div.desc-text').text().trim(),
status: loadedCheerio('h3:contains("Status")').next().text(),
chapters: [],
};
novel.author = loadedCheerio('h3:contains("Author")')
.parent()
.contents()
.text()
.replace('Author:', '');
novel.genres = loadedCheerio('h3:contains("Genre")')
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
const novelId = loadedCheerio('#rating').attr('data-novel-id')!;
const chapter: Plugin.ChapterItem[] = [];
const getChapters = async (id: string) => {
const chapterListUrl = this.site + 'ajax/chapter-option?novelId=' + id;
const data = await fetchApi(chapterListUrl);
const chapterlist = await data.text();
loadedCheerio = parseHTML(chapterlist);
loadedCheerio('select > option').each(function () {
const chapterName = loadedCheerio(this).text();
const chapterUrl = loadedCheerio(this).attr('value')?.slice(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
});
});
return chapter;
};
novel.chapters = await getChapters(novelId);
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio('#chapter-content div.ads').remove();
const chapterText = loadedCheerio('#chapter-content').html() || '';
return chapterText.replace(
/If you find any errors \(\s*Ads popup, ads redirect, broken links, non-standard content, etc\.\.\s*\), Please let us know \S* report chapter \S* so we can fix it as soon as possible\./,
'',
);
}
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search?keyword=${encodeURIComponent(searchTerm)}&page=${page}`;
const result = await fetchApi(searchUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
filters = {
sort: {
value: 'most-popular',
label: 'Sort by',
options: [
{ label: 'Latest Release', value: 'latest-release-novel' },
{ label: 'Hot Novel', value: 'hot-novel' },
{ label: 'Completed Novel', value: 'completed-novel' },
{ label: 'Most Popular', value: 'most-popular' },
],
type: FilterTypes.Picker,
},
genre: {
value: '',
label: 'Genres',
options: [
{ label: 'None', value: '' },
{ label: 'Shounen', value: 'Shounen' },
{ label: 'Harem', value: 'Harem' },
{ label: 'Comedy', value: 'Comedy' },
{ label: 'Martial Arts', value: 'Martial+Arts' },
{ label: 'School Life', value: 'School+Life' },
{ label: 'Mystery', value: 'Mystery' },
{ label: 'Shoujo', value: 'Shoujo' },
{ label: 'Romance', value: 'Romance' },
{ label: 'Sci-fi', value: 'Sci-fi' },
{ label: 'Gender Bender', value: 'Gender+Bender' },
{ label: 'Mature', value: 'Mature' },
{ label: 'Fantasy', value: 'Fantasy' },
{ label: 'Horror', value: 'Horror' },
{ label: 'Drama', value: 'Drama' },
{ label: 'Tragedy', value: 'Tragedy' },
{ label: 'Supernatural', value: 'Supernatural' },
{ label: 'Ecchi', value: 'Ecchi' },
{ label: 'Xuanhuan', value: 'Xuanhuan' },
{ label: 'Adventure', value: 'Adventure' },
{ label: 'Action', value: 'Action' },
{ label: 'Psychological', value: 'Psychological' },
{ label: 'Xianxia', value: 'Xianxia' },
{ label: 'Wuxia', value: 'Wuxia' },
{ label: 'Historical', value: 'Historical' },
{ label: 'Slice of Life', value: 'Slice+of+Life' },
{ label: 'Seinen', value: 'Seinen' },
{ label: 'Lolicon', value: 'Lolicon' },
{ label: 'Adult', value: 'Adult' },
{ label: 'Josei', value: 'Josei' },
{ label: 'Sports', value: 'Sports' },
{ label: 'Smut', value: 'Smut' },
{ label: 'Mecha', value: 'Mecha' },
{ label: 'Yaoi', value: 'Yaoi' },
{ label: 'Shounen Ai', value: 'Shounen+Ai' },
{ label: 'History', value: 'History' },
{ label: 'Martial', value: 'Martial' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new NovelFull();