From dd5322c984f10d9cbe08d204bd999b4a532fc7e3 Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Sat, 24 May 2025 17:21:59 +0300 Subject: [PATCH] added NovelCool source (#1693) * novelcool * fix * del --- .../multisrc/novelcool/novelcool/icon.png | Bin 0 -> 3630 bytes scripts/multisrc/novelcool/generator.js | 40 +++ scripts/multisrc/novelcool/sources.json | 14 + scripts/multisrc/novelcool/template.ts | 321 ++++++++++++++++++ 4 files changed, 375 insertions(+) create mode 100644 public/static/multisrc/novelcool/novelcool/icon.png create mode 100644 scripts/multisrc/novelcool/generator.js create mode 100644 scripts/multisrc/novelcool/sources.json create mode 100644 scripts/multisrc/novelcool/template.ts diff --git a/public/static/multisrc/novelcool/novelcool/icon.png b/public/static/multisrc/novelcool/novelcool/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..568613b2b58d77cfb7846e833a37931f47d76e67 GIT binary patch literal 3630 zcmY*c2{aU3`=2rPU6ds>#uj2`FoR)iSzlyHSzcscv(7MMpOH#YlP$7~>}$#{V+qMx z7-f$_vL>Oy$M=8F`e3GxcSdinc-&orH!{e!V+2n2eT1o}^w)AFGI)^|$& z?U0+_XjxWFp#T8Z+lD$?mUxHtY^EpY-wO3;IBx1bO*JxOZ(3qx765Y8#KZ*^ z!W@;y#Ns5R3gFUO+zCB8@pVOtE}V2;lp6G|e?E?{`9M zz;jA7P&JC~C#9nh_gmkr5G@Mm&B0x2`$qIhc(aE?n+q)$(R}Wh#29BlAUvRDO#dGk zasVG&eok%HU*84&g#)dfu-cKDLR zhe6>ufpCw2L?=^ED(z$Q!h(TWu1ZXzeP(b`q8w1}L_DD1>u@aaW;>&r{JA{(zON6w z!@N+p#M=vACZ;iDAs8EU2q5j?8s9~u$$S_xr_25rf(L*50ZAig;B z#2~+h0zQfwOIBdcCusj~5QzhbYy~M6awUJgJj}IdD0ZBBoe0!GtUfk;1jsz5SL?Ru z7kG5#lHNWfWqs#r5#-k!cD`S+h9rg54VPgF@Z9nN$!zP$PVX8}F58uRX2F4gfC3ft zYQ{W9$NT0A`Nkg}!4KE89o41?^}LfpuHPt=dspBa-OxY%1TCdHBy~cfv7-%uo1`~m zv-P#m0=>hTel~!A&U{sn@Hoo&cbd~hYYl@H2s7FZJ@P8qWS2Ar+qF5Q@+sI(_3#D- z^YcW@+YjqK4a*n6zn|iX2~7eXnURX5^V%~b=>pQVl{>Qme{eMmk*VrdA_khSThlvX zn0pz%qZ^RCWxl)t;;6iJO-P-+O~Z)9es4D5y}NnrIpNwOc%zFYH~R;nK_NfxzSr#@ zZ1d5&$S523UU?X=&S z+(rH{_j@|P1D_`90VUHH?kcZpY5aTYf}$g~_u=CipTT-lK_+s1*?fE6j44E=f-dvs z=sdDd#V|x#dY~u)CDSaID`a0>a-na}s5o<-TBNxQymEP<{1$dv zR66iD&=Pcekq5aVTk|KfPA&uI!stbbT>KUz#KdXAlXL-oJK_4|#Ml^T;w(*E_qFTp zRrX9At5D6pvm39-3r|o?v>k7bmpf zuBMrtbjhu*p>%4*(6emTOHUX^L{oY^CUxG+^=xxwyZ8YO3X`?c3P)dN&e06DPDQe({xvI^AC4oyw_xBh2)2t((NOWQ4zsM z0ctFCLPV$Ez?pT=37=%b>uO+b1p<=wqZJ~w$ybnf;dW~wo9n2@n$MEAI9*}>M7z_|;CLd(+^$DNZ>%V< zuwq8f&x<7&Y~+6q%6=7?JXWS!+?2ED>zWd5>84zDj^Kg-zr=Q{P|k_)%%8+>F+E9X zDQ$^00`O~ukSpS+V_-8%MobImG?S(K)!}Q}b$s79x}cJ}jkJC4A;>njFZg`rsllZ? zBj;WUkY~3fFA_njgCV^@3D)r|%ZlQzzJSOPBAPrAD|I)rr^eo3&YM#nILTZt5*42D z>B5meqG@;9;_w$&r#PX{Y4g`DBSITi_j&Lo$1Nzqd1lJAoevQ~)7N)EZP;S3lcJvm z@#N45O4l*1US*^MH&xKdIM6;Fs$p{da9`8q-V1?7Q_)T-g144~#1_@$+g@#-KRY#` z^x4!iJ%mUh4JTWM=Rm|?e;=MyN|<0&FI@(r>^eOkK=yOIh1C|$Ypw(qe6c4d93zRY zN;H1XqBEOed4_*{>l~UWiEj=2b-PX^3){1nI??B|wR>buw;|pVVCfdX)y2g7a zDet>=-9Mz6dYb}F6b9Tce=589sO#iiUVe9*VgN(4*f`<4Uj4(SXqJm>>px7&q2~1@ zn-6zr*vY$Zoc8wvS9R}9F^DR5TieoBI5G;D z%lgrAHe6x&Tz9`DTtUEjjFCb$Bu}*ir9a|sE7?r%#+E#Wz$V|z$*&6K-i<8MVuD}gsY)v-@*(ff3$GizyS zE1I_JrHs7$b3yn zLDE7iv^13E>1#<-hD|Fm9`?PqX+i-uv+~Jy|L>t3OA34?Bm$)brLmp`HmPo@-JZda`LOXP7JxThsPW) zKgu`SXK%f4kdG*hhOn(mOn@swA7CNA5| z!q764OL?kOk5d&j6U28@RWA$weiWHRpVv`m%{;zhZy>rW$dVP@WEC(-9TBiCbdVqo zCai?kVWjZ9%rhB?8>WjT5}wj8*ry&Wo2iG}z5OsuBUY>UZUD~iqbcbAa&C}3G(#@V z425rBhuI%g*C<^2i^U_Mr?|){Dd! zH`I@1?5Z8RG1@JbbzMeZE;Y#u{ViD6h23slQFyo#>i}5mh?L?(1xga+i>bYT4S;wC zy+qKXD{i)btdoMXcZ#3KRO!ko3+LpR;y&Kv>Up?KTvttqvtgBKV?gcC2p-M{LNmYm z9j>mb6}`a_UK=l)2rf~Yt*(|esl8U61wp2l527q?x~f;(P~;m!jIicf(Pj1eY59~q zuUqmQ=~)0A&E>tD`42Rz5%Y4I)xMA6%icf6;`%2r_i0QcLHN{#f{Zw=kZ)M_i<>zG zgE(yMk*~GX;u?jdHaED5Ms!A;1U56G&lkk$V`3Ix*!20X_NzNytr{LG%6!)4cKCMp zCo8k-{&O}mHx^z;|57MJ*QyKF%uN{=HcWe~v3!7+8*j7__MaDfs6AJRG33-As}wPO zu>Fi-$2#LY+qlzn_yQqtIR=shIR|!x0BI!t1vA144R>=7PqPZ);Ox_8sC!MPO4~8! EKW`4A6#xJL literal 0 HcmV?d00001 diff --git a/scripts/multisrc/novelcool/generator.js b/scripts/multisrc/novelcool/generator.js new file mode 100644 index 0000000..d9d05c9 --- /dev/null +++ b/scripts/multisrc/novelcool/generator.js @@ -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, + }; +}; diff --git a/scripts/multisrc/novelcool/sources.json b/scripts/multisrc/novelcool/sources.json new file mode 100644 index 0000000..ea4aa15 --- /dev/null +++ b/scripts/multisrc/novelcool/sources.json @@ -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" } + } +] diff --git a/scripts/multisrc/novelcool/template.ts b/scripts/multisrc/novelcool/template.ts new file mode 100644 index 0000000..ace9141 --- /dev/null +++ b/scripts/multisrc/novelcool/template.ts @@ -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; +}; + +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, + ): Promise { + 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 { + 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 { + 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 { + 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; +};