added NovelCool source (#1693)

* novelcool

* fix

* del
This commit is contained in:
Rider21
2025-05-24 17:21:59 +03:00
committed by GitHub
parent 3088f6882d
commit dd5322c984
4 changed files with 375 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+40
View File
@@ -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,
};
};
+14
View File
@@ -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" }
}
]
+321
View File
@@ -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;
};