From c6c5c99c1c054edb7ace34aa29d2db4d7c2f45e6 Mon Sep 17 00:00:00 2001 From: Rider21 <58046032+Rider21@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:12:52 +0300 Subject: [PATCH] Feat(plugin)Add Foxteller (#984) * test * optimization * terser@5.31.0 => terser@5.31.1 --- icons/multisrc/madara/worldnovel/icon.png | Bin 2295 -> 2290 bytes icons/src/en/foxteller/icon.png | Bin 0 -> 1984 bytes package-lock.json | 18 +- package.json | 2 +- plugins/english/foxteller.ts | 312 ++++++++++++++++++++++ scripts/multisrc/lightnovelwp/template.ts | 9 +- scripts/terser.ts | 3 - 7 files changed, 325 insertions(+), 19 deletions(-) create mode 100644 icons/src/en/foxteller/icon.png create mode 100644 plugins/english/foxteller.ts diff --git a/icons/multisrc/madara/worldnovel/icon.png b/icons/multisrc/madara/worldnovel/icon.png index d54a996ba3b92de82169fe54aabfcb02258b8ffe..f5b90a42f26d936cd51e6c465d64905d05b8cd8d 100644 GIT binary patch delta 2267 zcmV<12qgFS5%Lj`FMoqHoB#j+gEX6iHk*Sqn}ai(gEgCjG@Aeb0Dvo*?dk6J^Y?Re zbDNx-!^6WrKR*=}6tJ+cSy@@%-`|RgiYO>3($dn8N2Q=*SCB_Tln{Mj@Phq5TDJc^<`9JW2VNqSN3j;FVA+*L*SFQk{n z4&|M3oGvut7euMbzcYlTqvY%Jal4B+?zhMDia7A&?l0^pt0PIjKJH@wF~t3GWsK@~ z5akalf4bf#gnws{8(_e+pYI_K6)FF?&r9cVe~=kpT5%{)`|C}viN_&tSG1z|g6;!& z=X`l=5>K9bprf|?sX*U9H;X4Lo(NHFQy;y5S@r$O2q^zLlUSnf7g8?H!GC~VKv`vV7i);U(Cl( z2b!b&eQ#^E1^}@%nbRfY?@at70nzuqR_i%eo2u0P^zNi;-a&(hnhkk?dT1w}Z;=EH z&pBu>Ax0g25=!-xQ-U7jf}NeXyqW+EKjst)Lk!L6(AqIIqV8NB9>DY1fb&((S+-|G)InIkm#3lVK@L1Zrt;GPwO*3ngA03|EdW75_*(8Cg9*FSMNmDP@&wk2 z#pD8xtZ*3s=Jx;~uq4H*T3`;KqIQZ)@URUNf6qgqX3F-%{2pqepn_3kiqUm-JtYX9 z_kT-4B!D9)jY)F=AOwoLO!E$t47tp{7Tutkv20Vi525L5~~ zPQ~Di$7po5$FC12q2fA)_&yu4AL2rUD6v z9WMk}a#(GNe5ci2M=XhgTJ5P|LzIA~zO#Ua%c-I?!DWQVpnW3CMOy1sjF&!n5*?S)1D@_w{Gm1oqxOtlC%qgmM`%b2 zM`F230OW)bRisvL*;x)<(smxThyJlcWn1e(a-9-UF^9=!5U3(RT#S)rh&Ix>Gf^)z z7%Pa1R0nPV6#XhuaE=ET*;0`|Fn_wmFse?mog>=<`;chF1a6~4b!@#*&7PNlm}oX- zI>mYDon_sidip>Bsb3`9T2jDpDg^c$K9A;kty8+UIla+a9Nn2BG}>W6D~QvP1vJA+ z4xm1A*Cney~BxFS302~1{tbZ^~B*{wX zI_84BoW}s@29qQ@p2EpNZ1!3yohp>g%b(1u>%DAsiws%Ybq=0+0}!ZyjX~D1GW1|D zhQtNu+PJPbpBHcn4xE-MY=dOemp6cv!sG>tRzrEOnf<Vg>o_xq3&E zPYJ*!QxoMDQLOgh;SQ>hYJXcZ*%}BjTJr=XbOPW!IH#`nrOKsU87@$O<_!U3v0G_R zTX0ZxAORG^zJtA)*01PV>Zf&`S_@F(wL$yvn5(-G7nb|^6@W=d3a02VQEXJ=tp{k| z`$5Q8)y7r)hP9|1HSXw^{6L8L(lJ2lf1?Ugd#?iOOFy(&<^skJIDb*A$P<<8!*R4X zZo6Tt%?n#D4auTd1^@kX}Z4YXyF09a+^mB&S7_&*}TStY+nHkg&F*&A#Rn_ z6cij0wu8^`(Igr#XYB2&xy@5tdBOw$_8lHtYHKX5zsg|!ZA0BtC`JQd(f=9RmWiQq z+Km9H7Y4^@qR90QN`IDs;+a>PD1QhGz(EQGfh}^t|DR#U* zt_?aynn4qAHwE8-dowh@<-!&=nUVnnb4Zo13a z5O_Vp1F#g_DlW=GJ6tb93~rG(APaGnMxR5k{oF3}?zQ|i%3zLp3<&?{ zH5XsxoVQX_#uerlBeYDN#cL;(g3(AzL$U) z_X!Yrk5tlsCq7zH4KHr+dmB>nn;XN68^3(3BHKZ4Z}6)pr-@fjzNB^v0WgZ6sh4Md p{>{rX5vD9hZ~s(PQEq)}=09sG)S)LVU5Wqz002ovPDHLkV1lN=Ripp_ delta 2272 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, + ): Promise { + 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(/
([\s\S]*?)<\/div>/g) || []; + div.forEach(elements => { + const [, novelUrl, novelName] = + elements.match(//) || []; + + if (novelName && novelUrl) { + const novelCover = elements.match( + //, + ); + + novels.push({ + name: novelName, + cover: novelCover?.[1] || defaultCover, + path: novelUrl.split('/')[4], + }); + } + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + 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 { + 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 { + 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(/([\s\S]*?)<\/a>/g) || []; + items.forEach(elements => { + const novelUrl = elements.match(/(.*?)<\/span>/, + )?.[1]; + + if (name && path) { + const cover = + elements.match(//)?.[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; +} diff --git a/scripts/multisrc/lightnovelwp/template.ts b/scripts/multisrc/lightnovelwp/template.ts index 97c769c..0ed9f14 100644 --- a/scripts/multisrc/lightnovelwp/template.ts +++ b/scripts/multisrc/lightnovelwp/template.ts @@ -77,8 +77,8 @@ class LightNovelWPPlugin implements Plugin.PluginBase { const articles = html.match(//g) || []; articles.forEach(article => { - const novelName = article.match(/