Feat(plugin)Add Foxteller (#984)
* test * optimization * terser@5.31.0 => terser@5.31.1
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Generated
+9
-9
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lnreader-plugins",
|
"name": "lnreader-plugins",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lnreader-plugins",
|
"name": "lnreader-plugins",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"protobufjs": "^7.2.6",
|
"protobufjs": "^7.2.6",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
"terser": "^5.31.0",
|
"terser": "^5.31.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
@@ -4121,9 +4121,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.31.0",
|
"version": "5.31.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz",
|
||||||
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
"integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
@@ -7708,9 +7708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terser": {
|
"terser": {
|
||||||
"version": "5.31.0",
|
"version": "5.31.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz",
|
||||||
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
"integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
"protobufjs": "^7.2.6",
|
"protobufjs": "^7.2.6",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
"terser": "^5.31.0",
|
"terser": "^5.31.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { Plugin } from '@typings/plugin';
|
||||||
|
import { Parser } from 'htmlparser2';
|
||||||
|
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||||
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
|
import { fetchApi } from '@libs/fetch';
|
||||||
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
|
class Foxteller implements Plugin.PluginBase {
|
||||||
|
id = 'foxteller';
|
||||||
|
name = 'Foxteller';
|
||||||
|
site = 'https://www.foxteller.com';
|
||||||
|
version = '1.0.0';
|
||||||
|
icon = 'src/en/foxteller/icon.png';
|
||||||
|
|
||||||
|
async popularNovels(
|
||||||
|
pageNo: number,
|
||||||
|
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
|
const url =
|
||||||
|
this.site + '/library?sort=' + filters?.order?.value || 'popularity';
|
||||||
|
|
||||||
|
const body = await fetchApi(url).then(res => res.text());
|
||||||
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
|
const div = body.match(/<div class="col-md-6">([\s\S]*?)<\/div>/g) || [];
|
||||||
|
div.forEach(elements => {
|
||||||
|
const [, novelUrl, novelName] =
|
||||||
|
elements.match(/<a href="(.*?)" title="(.*?)">/) || [];
|
||||||
|
|
||||||
|
if (novelName && novelUrl) {
|
||||||
|
const novelCover = elements.match(
|
||||||
|
/<img class="img-fluid" src="(.*?)".*>/,
|
||||||
|
);
|
||||||
|
|
||||||
|
novels.push({
|
||||||
|
name: novelName,
|
||||||
|
cover: novelCover?.[1] || defaultCover,
|
||||||
|
path: novelUrl.split('/')[4],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return novels;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
|
const body = await fetchApi(this.resolveUrl(novelPath)).then(res =>
|
||||||
|
res.text(),
|
||||||
|
);
|
||||||
|
const novel: Plugin.SourceNovel = {
|
||||||
|
path: novelPath,
|
||||||
|
name: '',
|
||||||
|
genres: '',
|
||||||
|
summary: '',
|
||||||
|
status: '',
|
||||||
|
chapters: [] as Plugin.ChapterItem[],
|
||||||
|
};
|
||||||
|
let isParsingGenres = false;
|
||||||
|
let isReadingGenre = false;
|
||||||
|
let isReadingSummary = false;
|
||||||
|
let isParsingInfo = false;
|
||||||
|
let isReadingInfo = false;
|
||||||
|
let isParsingChapterList = false;
|
||||||
|
let isReadingChapter = false;
|
||||||
|
let isPaidChapter = false;
|
||||||
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
let tempChapter = {} as Plugin.ChapterItem;
|
||||||
|
|
||||||
|
const parser = new Parser({
|
||||||
|
onopentag(name, attribs) {
|
||||||
|
// name and cover
|
||||||
|
if (!novel.cover && attribs['class'] === 'img-fluid') {
|
||||||
|
novel.name = attribs['alt'];
|
||||||
|
novel.cover = attribs['src'] || defaultCover;
|
||||||
|
} // genres
|
||||||
|
else if (name === 'div' && attribs['class'] === 'novel-genres') {
|
||||||
|
isParsingGenres = true;
|
||||||
|
} else if (isParsingGenres && name === 'li') {
|
||||||
|
isReadingGenre = true;
|
||||||
|
} // summary
|
||||||
|
else if (name === 'div' && attribs['class'] === 'novel-description') {
|
||||||
|
isReadingSummary = true;
|
||||||
|
} // status
|
||||||
|
else if (name === 'div' && attribs['class'] === 'novel-tags') {
|
||||||
|
isParsingInfo = true;
|
||||||
|
} else if (isParsingInfo && name === 'li') {
|
||||||
|
isReadingInfo = true;
|
||||||
|
}
|
||||||
|
// chapters
|
||||||
|
else if (name === 'div' && attribs['class'] === 'col-md-6') {
|
||||||
|
isParsingChapterList = true;
|
||||||
|
} else if (isParsingChapterList && name === 'a') {
|
||||||
|
isReadingChapter = true;
|
||||||
|
tempChapter.chapterNumber = chapters.length + 1;
|
||||||
|
tempChapter.path = novelPath + '/' + attribs['href'].split('/')[5];
|
||||||
|
} else if (
|
||||||
|
isReadingChapter &&
|
||||||
|
name === 'i' &&
|
||||||
|
attribs['class']?.includes('lock')
|
||||||
|
) {
|
||||||
|
isPaidChapter = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ontext(data) {
|
||||||
|
// genres
|
||||||
|
if (isParsingGenres) {
|
||||||
|
if (isReadingGenre) {
|
||||||
|
novel.genres += data + ', ';
|
||||||
|
}
|
||||||
|
} // summary
|
||||||
|
else if (isReadingSummary) {
|
||||||
|
novel.summary += data.trim();
|
||||||
|
} // status
|
||||||
|
else if (isParsingInfo) {
|
||||||
|
if (isReadingInfo) {
|
||||||
|
const detailName = data.toLowerCase().trim();
|
||||||
|
switch (detailName) {
|
||||||
|
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;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// chapters
|
||||||
|
else if (isParsingChapterList) {
|
||||||
|
if (isReadingChapter) {
|
||||||
|
tempChapter.name = (tempChapter.name || '') + data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclosetag(name) {
|
||||||
|
// genres
|
||||||
|
if (isParsingGenres) {
|
||||||
|
if (isReadingGenre) {
|
||||||
|
isReadingGenre = false; // stop reading genre
|
||||||
|
} else {
|
||||||
|
isParsingGenres = false; // stop parsing genres
|
||||||
|
novel.genres = novel.genres?.slice(0, -2); // remove trailing comma
|
||||||
|
}
|
||||||
|
} // summary
|
||||||
|
else if (isReadingSummary) {
|
||||||
|
if (name === 'hr' || name === 'p') {
|
||||||
|
novel.summary += '\n';
|
||||||
|
} else if (name === 'div') {
|
||||||
|
isReadingSummary = false;
|
||||||
|
}
|
||||||
|
} // status
|
||||||
|
else if (isParsingInfo) {
|
||||||
|
if (name === 'li') {
|
||||||
|
isReadingInfo = false;
|
||||||
|
} else if (name === 'div') {
|
||||||
|
isParsingInfo = false;
|
||||||
|
}
|
||||||
|
} // chapters
|
||||||
|
else if (isParsingChapterList) {
|
||||||
|
if (isReadingChapter) {
|
||||||
|
if (name === 'li') {
|
||||||
|
if (isPaidChapter) {
|
||||||
|
isPaidChapter = false;
|
||||||
|
} else {
|
||||||
|
tempChapter.name = tempChapter.name.trim();
|
||||||
|
chapters.push(tempChapter);
|
||||||
|
}
|
||||||
|
isReadingChapter = false;
|
||||||
|
tempChapter = {} as Plugin.ChapterItem;
|
||||||
|
}
|
||||||
|
} else if (name === 'ul') {
|
||||||
|
isParsingChapterList = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.write(body);
|
||||||
|
parser.end();
|
||||||
|
|
||||||
|
novel.chapters = chapters;
|
||||||
|
|
||||||
|
return novel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
|
const res = await fetchApi(this.resolveUrl(chapterPath));
|
||||||
|
const body = await res.text();
|
||||||
|
const novelID = chapterPath.split('/')[0];
|
||||||
|
const chapterID = body.match(/'chapter_id': '([\d]+)'/)?.[1];
|
||||||
|
|
||||||
|
if (!chapterID) throw new Error('No chapter found');
|
||||||
|
|
||||||
|
const { aux } = await fetchApi(this.site + '/aux_dem', {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||||
|
'Content-Type': 'application/json;charset=utf-8',
|
||||||
|
},
|
||||||
|
Referer: res.url,
|
||||||
|
body: JSON.stringify({ 'x1': novelID, 'x2': chapterID }),
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
if (aux && typeof aux === 'string') {
|
||||||
|
const base64 = aux.replace(/%R([a-f])&/g, (match, code) => {
|
||||||
|
switch (code) {
|
||||||
|
case 'a':
|
||||||
|
return 'A';
|
||||||
|
case 'c':
|
||||||
|
return 'B';
|
||||||
|
case 'b':
|
||||||
|
return 'C';
|
||||||
|
case 'd':
|
||||||
|
return 'D';
|
||||||
|
case 'f':
|
||||||
|
return 'E';
|
||||||
|
case 'e':
|
||||||
|
return 'F';
|
||||||
|
default:
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return decodeURIComponent(escape(decodeBase64(base64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('This chapter is closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||||
|
const body = await fetchApi(this.site + '/search', {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||||
|
'Content-Type': 'application/json;charset=utf-8',
|
||||||
|
},
|
||||||
|
Referer: this.site,
|
||||||
|
body: JSON.stringify({ query: searchTerm }),
|
||||||
|
}).then(res => res.text());
|
||||||
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
|
const items = body.match(/<a.*>([\s\S]*?)<\/a>/g) || [];
|
||||||
|
items.forEach(elements => {
|
||||||
|
const novelUrl = elements.match(/<a href="(.*?)"/)?.[1] || '';
|
||||||
|
const path = novelUrl.split('/')[4];
|
||||||
|
const name = elements.match(
|
||||||
|
/<span class="ellipsis-1">(.*?)<\/span>/,
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
if (name && path) {
|
||||||
|
const cover =
|
||||||
|
elements.match(/<img src="(.*?)".*>/)?.[1] || defaultCover;
|
||||||
|
novels.push({ name, cover, path });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return novels;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
|
this.site + '/novel/' + path;
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
order: {
|
||||||
|
value: 'popularity',
|
||||||
|
label: 'Order by',
|
||||||
|
options: [
|
||||||
|
{ label: 'Popular Novels', value: 'popularity' },
|
||||||
|
{ label: 'New Novels', value: 'newest' },
|
||||||
|
],
|
||||||
|
type: FilterTypes.Picker,
|
||||||
|
},
|
||||||
|
} satisfies Filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Foxteller();
|
||||||
|
|
||||||
|
const base64Characters =
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
|
|
||||||
|
function decodeBase64(encodedString: string) {
|
||||||
|
let output = '';
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < encodedString.length) {
|
||||||
|
const enc1 = base64Characters.indexOf(encodedString.charAt(i++));
|
||||||
|
const enc2 = base64Characters.indexOf(encodedString.charAt(i++));
|
||||||
|
const enc3 = base64Characters.indexOf(encodedString.charAt(i++));
|
||||||
|
const enc4 = base64Characters.indexOf(encodedString.charAt(i++));
|
||||||
|
|
||||||
|
const chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||||
|
output += String.fromCharCode(chr1);
|
||||||
|
|
||||||
|
if (enc3 !== 64) {
|
||||||
|
const chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||||
|
output += String.fromCharCode(chr2);
|
||||||
|
}
|
||||||
|
if (enc4 !== 64) {
|
||||||
|
const chr3 = ((enc3 & 3) << 6) | enc4;
|
||||||
|
output += String.fromCharCode(chr3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@@ -77,8 +77,8 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const articles = html.match(/<article([\s\S]*?)<\/article>/g) || [];
|
const articles = html.match(/<article([\s\S]*?)<\/article>/g) || [];
|
||||||
articles.forEach(article => {
|
articles.forEach(article => {
|
||||||
const novelName = article.match(/<a.*title="(.*?)"/)?.[1];
|
const [, novelUrl, novelName] =
|
||||||
const novelUrl = article.match(/<a href="(.*?)"/)?.[1];
|
article.match(/<a href="(.*?)".*title="(.*?)"/) || [];
|
||||||
|
|
||||||
if (novelName && novelUrl) {
|
if (novelName && novelUrl) {
|
||||||
const novelCover =
|
const novelCover =
|
||||||
@@ -148,10 +148,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
|
|||||||
const parser = new Parser({
|
const parser = new Parser({
|
||||||
onopentag(name, attribs) {
|
onopentag(name, attribs) {
|
||||||
// name and cover
|
// name and cover
|
||||||
if (
|
if (!novel.cover && attribs['class']?.includes('ts-post-image')) {
|
||||||
!novel.cover &&
|
|
||||||
attribs['class']?.includes('ts-post-image' || 'wp-post-image')
|
|
||||||
) {
|
|
||||||
novel.name = attribs['title'];
|
novel.name = attribs['title'];
|
||||||
novel.cover = attribs['data-src'] || attribs['src'] || defaultCover;
|
novel.cover = attribs['data-src'] || attribs['src'] || defaultCover;
|
||||||
} // genres
|
} // genres
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ import * as fs from 'fs';
|
|||||||
const config = {
|
const config = {
|
||||||
compress: {
|
compress: {
|
||||||
arrows: false,
|
arrows: false,
|
||||||
booleans_as_integers: true,
|
|
||||||
},
|
},
|
||||||
mangle: {},
|
mangle: {},
|
||||||
ecma: 5, // specify one of: 5, 2015, 2016, etc.
|
ecma: 5, // specify one of: 5, 2015, 2016, etc.
|
||||||
enclose: false, // or specify true, or "args:values"
|
enclose: false, // or specify true, or "args:values"
|
||||||
keep_classnames: false,
|
|
||||||
keep_fnames: false,
|
|
||||||
module: true,
|
module: true,
|
||||||
toplevel: true,
|
toplevel: true,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user