Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
import list from './sources.json' with { type: 'json' };
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// https://app.novelcool.com/
|
||||||
|
const app = {
|
||||||
|
userAgent:
|
||||||
|
'Android/Package:com.zuoyou.novel - Version Name:2.3 - Phone Info:sdk_gphone_x86_64(Android Version:13)',
|
||||||
|
package_name: 'com.zuoyou.novel',
|
||||||
|
appId: '202201290625004',
|
||||||
|
secret: 'c73a8590641781f203660afca1d37ada',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAll = function () {
|
||||||
|
list.sort((a, b) => a.id.length - b.id.length);
|
||||||
|
return list.map(source => generator(source));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = function generator(source) {
|
||||||
|
const NovelCoolTemplate = readFileSync(__dirname + '/template.ts', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
source.options.app = app;
|
||||||
|
|
||||||
|
const pluginScript = `
|
||||||
|
${NovelCoolTemplate}
|
||||||
|
|
||||||
|
const plugin = new NovelCoolPlugin(${JSON.stringify(source)});
|
||||||
|
export default plugin;
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
lang: source.options.lang,
|
||||||
|
filename: source.sourceName,
|
||||||
|
pluginScript,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "novelcool",
|
||||||
|
"sourceName": "NovelCool",
|
||||||
|
"sourceSite": "https://www.novelcool.com",
|
||||||
|
"options": { "lang": "English", "langCode": "en" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "novelcool-ru",
|
||||||
|
"sourceName": "NovelCool (RU)",
|
||||||
|
"sourceSite": "https://ru.novelcool.com",
|
||||||
|
"options": { "lang": "Russian", "langCode": "ru" }
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import { fetchApi } from '@libs/fetch';
|
||||||
|
import { Plugin } from '@typings/plugin';
|
||||||
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
type NovelCoolOptions = {
|
||||||
|
lang: string;
|
||||||
|
langCode: string;
|
||||||
|
app: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NovelCoolMetadata = {
|
||||||
|
id: string;
|
||||||
|
sourceSite: string;
|
||||||
|
sourceName: string;
|
||||||
|
options: NovelCoolOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NovelCoolPlugin implements Plugin.PluginBase {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
site: string;
|
||||||
|
mainUrl: string;
|
||||||
|
version: string;
|
||||||
|
options: NovelCoolOptions;
|
||||||
|
filters: Filters;
|
||||||
|
|
||||||
|
constructor(metadata: NovelCoolMetadata) {
|
||||||
|
this.id = metadata.id;
|
||||||
|
this.name = metadata.sourceName;
|
||||||
|
this.site = metadata.sourceSite;
|
||||||
|
this.icon = 'multisrc/novelcool/novelcool/icon.png';
|
||||||
|
this.mainUrl = 'https://api.novelcool.com';
|
||||||
|
this.version = '1.0.0';
|
||||||
|
this.options = metadata.options ?? ({} as NovelCoolOptions);
|
||||||
|
this.filters = {
|
||||||
|
sortby: {
|
||||||
|
label: 'Order by',
|
||||||
|
value: 'hot',
|
||||||
|
options: [
|
||||||
|
{ label: 'Hottest', value: 'hot' },
|
||||||
|
{ label: 'Latest', value: 'latest' },
|
||||||
|
{ label: 'New Books', value: 'new_book' },
|
||||||
|
],
|
||||||
|
type: FilterTypes.Picker,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async popularNovels(
|
||||||
|
page: number,
|
||||||
|
{
|
||||||
|
filters,
|
||||||
|
showLatestNovels,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
|
const sortby = showLatestNovels
|
||||||
|
? 'latest'
|
||||||
|
: filters?.sortby?.value || 'hot';
|
||||||
|
|
||||||
|
const { list }: { list: Novel[] } = await fetchApi(
|
||||||
|
this.mainUrl + '/elite/' + sortby,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.options.app.userAgent,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
appId: this.options.app.appId,
|
||||||
|
secret: this.options.app.secret,
|
||||||
|
package_name: this.options.app.package_name,
|
||||||
|
lc_type: 'novel',
|
||||||
|
lang: this.options.langCode,
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: '20',
|
||||||
|
}).toString(),
|
||||||
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
|
list.forEach(novel =>
|
||||||
|
novels.push({
|
||||||
|
name: novel.name,
|
||||||
|
cover: novel.cover,
|
||||||
|
path: novel.visit_path + '?id=' + novel.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return novels;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
|
const book_id = novelPath.split('?id=')[1];
|
||||||
|
const { info }: { info: Novel } = await fetchApi(
|
||||||
|
this.mainUrl + '/book/info/',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.options.app.userAgent,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
book_id,
|
||||||
|
appId: this.options.app.appId,
|
||||||
|
secret: this.options.app.secret,
|
||||||
|
package_name: this.options.app.package_name,
|
||||||
|
lang: this.options.langCode,
|
||||||
|
}).toString(),
|
||||||
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
|
||||||
|
const novel: Plugin.SourceNovel = {
|
||||||
|
path: novelPath,
|
||||||
|
name: info.name,
|
||||||
|
cover: info.cover,
|
||||||
|
genres: info.category_list.join(','),
|
||||||
|
summary: info.intro,
|
||||||
|
author: info.author,
|
||||||
|
artist: info.artist,
|
||||||
|
status:
|
||||||
|
info.completed === 'YES' ? NovelStatus.Completed : NovelStatus.Ongoing,
|
||||||
|
rating: (info.rate_star && parseFloat(info.rate_star)) || undefined,
|
||||||
|
};
|
||||||
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
|
||||||
|
const { list }: { list: ChapterNext[] } = await fetchApi(
|
||||||
|
this.mainUrl + '/chapter/book_list/',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.options.app.userAgent,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
book_id,
|
||||||
|
appId: this.options.app.appId,
|
||||||
|
secret: this.options.app.secret,
|
||||||
|
package_name: this.options.app.package_name,
|
||||||
|
lang: this.options.langCode,
|
||||||
|
}).toString(),
|
||||||
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
|
||||||
|
list.forEach(chapter => {
|
||||||
|
if (!chapter.is_locked) {
|
||||||
|
chapters.push({
|
||||||
|
name: chapter.title,
|
||||||
|
path: chapter.book_id + '/' + chapter.id,
|
||||||
|
releaseTime: dayjs(parseInt(chapter.last_modify, 10) * 1000).format(
|
||||||
|
'LLL',
|
||||||
|
),
|
||||||
|
chapterNumber: parseInt(chapter.order_id, 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
novel.chapters = chapters.reverse();
|
||||||
|
return novel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
|
const [, chapter_id] = chapterPath.split('/');
|
||||||
|
const { info }: { info: Chapter } = await fetchApi(
|
||||||
|
this.mainUrl + '/chapter/info/',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.options.app.userAgent,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
chapter_id,
|
||||||
|
appId: this.options.app.appId,
|
||||||
|
secret: this.options.app.secret,
|
||||||
|
package_name: this.options.app.package_name,
|
||||||
|
lang: this.options.langCode,
|
||||||
|
}).toString(),
|
||||||
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
|
||||||
|
return info.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchNovels(
|
||||||
|
searchTerm: string,
|
||||||
|
pageNo: number,
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
|
const { list }: { list: Novel[] } = await fetchApi(
|
||||||
|
this.mainUrl + '/book/search/',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.options.app.userAgent,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
appId: this.options.app.appId,
|
||||||
|
secret: this.options.app.secret,
|
||||||
|
package_name: this.options.app.package_name,
|
||||||
|
keyword: searchTerm,
|
||||||
|
lc_type: 'novel',
|
||||||
|
lang: this.options.langCode,
|
||||||
|
page: pageNo.toString(),
|
||||||
|
page_size: '20',
|
||||||
|
}).toString(),
|
||||||
|
},
|
||||||
|
).then(res => res.json());
|
||||||
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
|
list.forEach(novel =>
|
||||||
|
novels.push({
|
||||||
|
name: novel.name,
|
||||||
|
cover: novel.cover,
|
||||||
|
path: novel.visit_path + '?id=' + novel.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return novels;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
|
this.site + (isNovel ? '/novel/' : '/chapter/') + path.split('?id=')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Novel = {
|
||||||
|
id: string;
|
||||||
|
lang: string;
|
||||||
|
url: string;
|
||||||
|
visit_path: string;
|
||||||
|
name: string;
|
||||||
|
alternative: string;
|
||||||
|
publish_year: string;
|
||||||
|
warning: string;
|
||||||
|
char_index: string;
|
||||||
|
author: string;
|
||||||
|
artist: string;
|
||||||
|
intro: string;
|
||||||
|
tags?: string;
|
||||||
|
category: string;
|
||||||
|
completed: 'YES' | 'NO';
|
||||||
|
last_chapter_id: string;
|
||||||
|
last_chapter_title: string;
|
||||||
|
all_views: string;
|
||||||
|
rate_star: string;
|
||||||
|
rate_mark: string;
|
||||||
|
rate_num: string;
|
||||||
|
follow_num: string;
|
||||||
|
pic_num: string;
|
||||||
|
make_time: Date;
|
||||||
|
copy_limit: string;
|
||||||
|
copyright: string;
|
||||||
|
user_id: string;
|
||||||
|
show_it: 'ALLOW';
|
||||||
|
type: string;
|
||||||
|
elite: string;
|
||||||
|
book_id: string;
|
||||||
|
is_novel: string;
|
||||||
|
is_new: string;
|
||||||
|
is_hot: string;
|
||||||
|
cover: string;
|
||||||
|
og_url?: string;
|
||||||
|
no: string;
|
||||||
|
last_url?: string;
|
||||||
|
category_str: string;
|
||||||
|
category_list: string[];
|
||||||
|
star_list: any[];
|
||||||
|
int_mark: string;
|
||||||
|
time: string;
|
||||||
|
show_ads: string;
|
||||||
|
first_chapter_id?: string;
|
||||||
|
first_chapter_type?: number;
|
||||||
|
is_following: boolean;
|
||||||
|
copyright_limit: boolean;
|
||||||
|
share_title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Chapter = {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
vol_id: string;
|
||||||
|
lang: string;
|
||||||
|
order_id: string;
|
||||||
|
book_id: string;
|
||||||
|
title: string;
|
||||||
|
type: number;
|
||||||
|
url: string;
|
||||||
|
content: string;
|
||||||
|
content_size: string;
|
||||||
|
fixed: string;
|
||||||
|
group_name: string;
|
||||||
|
last_modify: string;
|
||||||
|
add_time: Date;
|
||||||
|
prev: ChapterNext;
|
||||||
|
next: ChapterNext;
|
||||||
|
blank: boolean;
|
||||||
|
o_url: string;
|
||||||
|
is_locked: boolean;
|
||||||
|
show_url: boolean;
|
||||||
|
is_following: boolean;
|
||||||
|
share_title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterNext = {
|
||||||
|
id: string;
|
||||||
|
book_id: string;
|
||||||
|
type: number;
|
||||||
|
lang: string;
|
||||||
|
vol_id: string;
|
||||||
|
order_id: string;
|
||||||
|
title: string;
|
||||||
|
last_modify: string;
|
||||||
|
url: string;
|
||||||
|
no: string;
|
||||||
|
is_locked: boolean;
|
||||||
|
is_new: boolean;
|
||||||
|
tf_time: string;
|
||||||
|
url_pre?: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user