Multisrc Readnovelfull: add novelfull (#1657)
* add novelfull * delete novelfull
This commit is contained in:
|
Before Width: | Height: | Size: 758 B After Width: | Height: | Size: 758 B |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
|
||||||
Reference in New Issue
Block a user