From d818d1758f5a13ff170a4231216f40211b832a9c Mon Sep 17 00:00:00 2001 From: nyagami Date: Sun, 25 Feb 2024 14:52:37 +0700 Subject: [PATCH] Beta phase (#801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * beta host * Refactor: url -> path (#757) * refactor: url -> path * clean up * fix: json branch * PagePlugin (#769) * paginated novel support * fix: truyenfull * fix: tunovel, remove pageList * parsePage support for webtest (#773) * add parsePage api * add parsePage UI * add totalPages * (FR src) Beta plugin & Added filters (#775) * noveldeglace: - added filters - added latest - defaultCover fallback - change id to noveldeglace.com - added complementary infos in summary - added cover artist - chapters are now parts - show volume number for multi volume novels - better search - fix search page number - refactored for plugin beta - releaseDate as Date.toString() - separted parseNovels - separated getCheerio - loadedCheerio => $ for shorter lines & simpler test on web * chireads: - fix different parsing for mobile version - added filters - added author - added status - added released date - refactored for plugin beta - separted parseNovels - separated getCheerio - loadedCheerio => $ for shorter lines & simpler test on web * wpmangastream => lightnovelwp rework & new sources (#776) * Added sources: - Knoxt (EN) - Baca Light Novel (ID) - AllNovelRead (ES) - DaoTranslate (EN) - جنة الروايات (AR) - Universal Novel (EN) - NovelTR (TR) old sources auto-generated: - GenesisTls (EN) - NobleMTL (EN) wpmangastream => lightnovelwp: - added doc - added filters in a separte file - semi-automatic filter generator - made it compatible with version 1.0.X and 1.1.X versions of the theme - loadedCheerio => $ - icons in "/icons/lightnovel/id.png" - code cleanup * removed auto-implemented sources * Added README.md to lightnovelwp * removed ai generated text for kolnovel * added novelsknight * added centralNovel * pandamtl accual name * adding folder plugin/arabic * Fix: Vietnamese plugins (#777) * fix: lightnovelvn * add nettruyen * add truyenchu * fix: truyenconect * Fix: Migrate plugins (#771) * Add novel.ovh (#756) * fix * test * fix * date fix + master merge (#761) * date fix * Added Brazilian Portuguese E-Novel (#758) * fixed single digit dates (#767) Co-authored-by: NaddH1 * fix * test * fix order * Update renovels.ts * require_chapters * Delete plugins/english/genesistl.broken.ts * Update novelTL.ts --------- Co-authored-by: NaddH1 * (src update) ENG (#772) * allnovelfull * bestlightnovel * divinedaolibrary * earlynovel * lightnovelpub * L * M * novelbin * novelfull * novelbuddy * N S * novel updates * async sleep * rename file * pagePlugin * Feat: minify js dir (#781) * Multisrc: HotNovelPub (#784) * HotNovelPub * fix search * Squashed commit of the following: commit 3b759c68eddc06c0adfb959f775de069a85adca7 Author: nyagami Date: Sat Feb 17 20:33:47 2024 +0700 Feat: minify js dir (#781) * jsmin changed to terser * test * why I did it. * test * Update ranobelib.ts * fix page * Update ranobelib.ts * updated modules * Update terser.json Co-authored-by: nyagami * Update scripts/multisrc/hotnovelpub/template.ts Co-authored-by: nyagami * terser api --------- Co-authored-by: nyagami * Added sources & fix sources (#787) * lightnovelwp: - fix author name - added more localisation of strings - README typo - error on !r.ok new message * noveldeglace: - fix chapterNumber for parts - better dates - better search * Added sources: - White Moonlight Novels (EN) - System Translation (EN) * Resized centralnovel.png * Add Faqwiki.us source (#792) * Add Faqwiki.us source (beta branch) * Fix Path, Fix Chapter Parsing, Fix Novel Title * search novel path fix * fix chapter parsing. some chapter in this site store their whole text in 1-4

, * Add WTR-LAB Source (#797) * Add WTR-LAB Source (Beta Branch) * Add other language from WTR-LAB source * change lang to sourceLang * remove id es tr lang from WTR-LAB * Update plugins/english/wtrlab.ts agree Co-authored-by: nyagami * Update plugins/english/wtrlab.ts agree Co-authored-by: nyagami --------- Co-authored-by: nyagami * revert: scripts to master --------- Co-authored-by: Justin COLLON <48163201+error7404@users.noreply.github.com> Co-authored-by: Rider21 <58046032+Rider21@users.noreply.github.com> Co-authored-by: NaddH1 Co-authored-by: K1ngfish3r Co-authored-by: bregassa --- docs/plugin-template.ts | 13 +- icons/multisrc/hotnovelpub/icons/eznovels.png | Bin 0 -> 59390 bytes .../hotnovelpub/icons/hotnovelpub.png | Bin 0 -> 5221 bytes icons/multisrc/hotnovelpub/icons/lanovels.png | Bin 0 -> 5780 bytes .../hotnovelpub/icons/lightnoveldaily.png | Bin 0 -> 49225 bytes icons/multisrc/hotnovelpub/icons/thnovels.png | Bin 0 -> 5780 bytes icons/multisrc/lightnovelwp/allnovelread.png | Bin 0 -> 2767 bytes .../multisrc/lightnovelwp/bacalightnovel.png | Bin 0 -> 3793 bytes icons/multisrc/lightnovelwp/centralnovel.png | Bin 0 -> 1697 bytes icons/multisrc/lightnovelwp/daotranslate.png | Bin 0 -> 7328 bytes icons/multisrc/lightnovelwp/genesistls.png | Bin 0 -> 21462 bytes icons/multisrc/lightnovelwp/knoxt.png | Bin 0 -> 8420 bytes icons/multisrc/lightnovelwp/kolnovel.png | Bin 0 -> 5023 bytes .../lightnovelwp/lightnovelbrasil.png | Bin 0 -> 5009 bytes icons/multisrc/lightnovelwp/lightnovelfr.png | Bin 0 -> 7333 bytes icons/multisrc/lightnovelwp/noblemtl.png | Bin 0 -> 4003 bytes icons/multisrc/lightnovelwp/novelsknight.png | Bin 0 -> 12183 bytes .../multisrc/lightnovelwp/novelsparadise.png | Bin 0 -> 12543 bytes icons/multisrc/lightnovelwp/noveltr.png | Bin 0 -> 10885 bytes icons/multisrc/lightnovelwp/pandamtl.png | Bin 0 -> 4119 bytes .../lightnovelwp/systemtranslation.png | Bin 0 -> 8933 bytes .../multisrc/lightnovelwp/universalnovel.png | Bin 0 -> 15555 bytes .../lightnovelwp/whitemoonlightnovels.png | Bin 0 -> 2625 bytes icons/src/en/genesistls/icon.png | Bin 14690 -> 0 bytes icons/src/en/lightnovelreader/icon.png | Bin 3927 -> 0 bytes icons/src/en/noblemtl/icon.png | Bin 12407 -> 0 bytes icons/src/en/novelbuddy/icon.png | Bin 0 -> 3198 bytes icons/src/en/novelfullme/icon.png | Bin 1540 -> 0 bytes icons/src/en/novelsonline/icon.png | Bin 0 -> 2469 bytes icons/src/en/readlitenovel/icon.png | Bin 0 -> 7185 bytes icons/src/en/wtrlab/icon.png | Bin 0 -> 3680 bytes icons/src/vi/nettruyen/icon.png | Bin 0 -> 7531 bytes icons/src/vi/truyenchu/icon.png | Bin 0 -> 8102 bytes index.ts | 30 +- package-lock.json | 258 +++- package.json | 19 +- plugins/arabic/.gitkeep | 0 plugins/arabic/kolnovel.ts | 315 ---- plugins/chinese/{69shu.ts => 69shu.broken.ts} | 0 .../{69xinshu.ts => 69xinshu.broken.ts} | 0 .../{linovelib.ts => linovelib.broken.ts} | 0 plugins/english/LightNovelReader.broken.ts | 170 --- plugins/english/NovelFullMe.ts | 168 --- plugins/english/NovelOnline.ts | 172 ++- plugins/english/allnovelfull.ts | 62 +- plugins/english/bestlightnovel.ts | 305 ++-- .../{comrademao.ts => comrademao.broken.ts} | 0 plugins/english/divinedaolibrary.ts | 54 +- plugins/english/earlynovel.ts | 131 +- plugins/english/faqwikius.ts | 85 +- plugins/english/freenovelupdates.broken.ts | 127 -- plugins/english/freewebnovel.ts | 48 +- plugins/english/genesistl.ts | 247 ---- plugins/english/lightnovelpub.ts | 211 ++- plugins/english/lightnovelworld.ts | 211 ++- plugins/english/lnmtl.broken.ts | 180 --- plugins/english/lnmtl.ts | 132 +- plugins/english/mtlnovel.ts | 156 +- plugins/english/mtlreader.ts | 96 +- plugins/english/nobleMTL.ts | 327 ---- plugins/english/novelbin.ts | 199 ++- plugins/english/novelbuddy.ts | 171 +++ plugins/english/novelforest.broken.ts | 167 --- plugins/english/novelfull.ts | 182 ++- plugins/english/novelhall.ts | 86 +- plugins/english/novelupdates.ts | 79 +- plugins/english/pandaMTL.ts | 239 --- plugins/english/readlitenovel.ts | 189 +++ plugins/english/scribblehub.ts | 51 +- plugins/english/wtrlab.ts | 286 ++++ .../{wuxiaworld.ts => wuxiaworld.broken.ts} | 0 plugins/french/chireads.ts | 304 +++- plugins/french/noveldeglace.ts | 452 ++++-- ...indowebnovel.ts => indowebnovel.broken.ts} | 0 .../{novelringan.ts => novelringan.broken.ts} | 0 .../{sakuranovel.ts => sakuranovel.broken.ts} | 0 .../{Syosetu.ts => Syosetu.broken.ts} | 0 .../korean/{Agitoon.ts => Agitoon.broken.ts} | 0 ...elbrasil.ts => lightnovelbrasil.broken.ts} | 0 plugins/russian/LitSpace.ts | 50 +- plugins/russian/authortoday.ts | 42 +- plugins/russian/bookriver.ts | 34 +- plugins/russian/ficbook.ts | 38 +- plugins/russian/jaomix.ts | 59 +- plugins/russian/novelOvh.ts | 24 +- plugins/russian/novelTL.ts | 41 +- plugins/russian/ranobehub.ts | 78 +- plugins/russian/ranobelib.ts | 163 +- plugins/russian/ranoberf.ts | 26 +- plugins/russian/renovels.ts | 67 +- .../spanish/{hasutl.ts => hasutl.broken.ts} | 0 ...velasligera.ts => novelasligera.broken.ts} | 0 .../{novelawuxia.ts => novelawuxia.broken.ts} | 0 ...lations.ts => oasistranslations.broken.ts} | 0 .../{skynovels.ts => skynovels.broken.ts} | 0 plugins/spanish/tunovelaligera.ts | 157 +- .../spanish/{yukitls.ts => yukitls.broken.ts} | 0 .../{epiknovel.ts => epiknovel.broken.ts} | 0 .../{lshnovel.ts => lshnovel.broken.ts} | 0 plugins/ukrainian/smakolykytl.ts | 50 +- plugins/vietnamese/LNHako.ts | 82 +- plugins/vietnamese/Truyenconect.broken.ts | 228 --- plugins/vietnamese/Truyenconect.ts | 299 ++++ plugins/vietnamese/lightnovelvn.broken.ts | 164 -- plugins/vietnamese/lightnovelvn.ts | 161 ++ plugins/vietnamese/nettruyen.ts | 108 ++ plugins/vietnamese/truyenchu.ts | 147 ++ plugins/vietnamese/truyenfull.ts | 68 +- scripts/json_plugins.ts | 23 +- scripts/multisrc/generate.ts | 3 +- .../multisrc/hotnovelpub/filter_refresh.ts | 83 ++ scripts/multisrc/hotnovelpub/generator.ts | 58 + scripts/multisrc/hotnovelpub/sources.json | 387 +++++ scripts/multisrc/hotnovelpub/template.ts | 279 ++++ scripts/multisrc/ifreedom/template.ts | 36 +- scripts/multisrc/lightnovelwp/README.md | 32 + .../lightnovelwp/filters/allnovelread.json | 1076 ++++++++++++++ .../lightnovelwp/filters/bacalightnovel.json | 78 + .../lightnovelwp/filters/centralnovel.json | 300 ++++ .../lightnovelwp/filters/daotranslate.json | 308 ++++ .../lightnovelwp/filters/genesistls.json | 240 +++ .../multisrc/lightnovelwp/filters/knoxt.json | 424 ++++++ .../lightnovelwp/filters/kolnovel.json | 268 ++++ .../lightnovelwp/filters/lightnovelfr.json | 248 ++++ .../lightnovelwp/filters/noblemtl.json | 492 ++++++ .../lightnovelwp/filters/novelsknight.json | 344 +++++ .../lightnovelwp/filters/novelsparadise.json | 156 ++ .../lightnovelwp/filters/noveltr.json | 252 ++++ .../lightnovelwp/filters/pandamtl.json | 156 ++ .../lightnovelwp/filters/universalnovel.json | 248 ++++ scripts/multisrc/lightnovelwp/generator.ts | 35 + scripts/multisrc/lightnovelwp/get_filters.ts | 103 ++ scripts/multisrc/lightnovelwp/sources.json | 146 ++ scripts/multisrc/lightnovelwp/template.ts | 234 +++ scripts/multisrc/madara/template.ts | 61 +- scripts/multisrc/readwn/template.ts | 58 +- scripts/multisrc/rulate/template.ts | 83 +- scripts/multisrc/wpmangastream/generator.ts | 30 - scripts/multisrc/wpmangastream/sources.json | 20 - scripts/multisrc/wpmangastream/template.ts | 149 -- scripts/terser.ts | 22 + test_web/api/plugins.ts | 33 +- test_web/index.html | 36 +- test_web/static/js/index.js | 135 +- test_web/static/js/index_old.js | 1315 ----------------- tsconfig.json | 2 +- types/plugin.ts | 75 +- 147 files changed, 10278 insertions(+), 5758 deletions(-) create mode 100644 icons/multisrc/hotnovelpub/icons/eznovels.png create mode 100644 icons/multisrc/hotnovelpub/icons/hotnovelpub.png create mode 100644 icons/multisrc/hotnovelpub/icons/lanovels.png create mode 100644 icons/multisrc/hotnovelpub/icons/lightnoveldaily.png create mode 100644 icons/multisrc/hotnovelpub/icons/thnovels.png create mode 100644 icons/multisrc/lightnovelwp/allnovelread.png create mode 100644 icons/multisrc/lightnovelwp/bacalightnovel.png create mode 100644 icons/multisrc/lightnovelwp/centralnovel.png create mode 100644 icons/multisrc/lightnovelwp/daotranslate.png create mode 100644 icons/multisrc/lightnovelwp/genesistls.png create mode 100644 icons/multisrc/lightnovelwp/knoxt.png create mode 100644 icons/multisrc/lightnovelwp/kolnovel.png create mode 100644 icons/multisrc/lightnovelwp/lightnovelbrasil.png create mode 100644 icons/multisrc/lightnovelwp/lightnovelfr.png create mode 100644 icons/multisrc/lightnovelwp/noblemtl.png create mode 100644 icons/multisrc/lightnovelwp/novelsknight.png create mode 100644 icons/multisrc/lightnovelwp/novelsparadise.png create mode 100644 icons/multisrc/lightnovelwp/noveltr.png create mode 100644 icons/multisrc/lightnovelwp/pandamtl.png create mode 100644 icons/multisrc/lightnovelwp/systemtranslation.png create mode 100644 icons/multisrc/lightnovelwp/universalnovel.png create mode 100644 icons/multisrc/lightnovelwp/whitemoonlightnovels.png delete mode 100644 icons/src/en/genesistls/icon.png delete mode 100644 icons/src/en/lightnovelreader/icon.png delete mode 100644 icons/src/en/noblemtl/icon.png create mode 100644 icons/src/en/novelbuddy/icon.png delete mode 100644 icons/src/en/novelfullme/icon.png create mode 100644 icons/src/en/novelsonline/icon.png create mode 100644 icons/src/en/readlitenovel/icon.png create mode 100644 icons/src/en/wtrlab/icon.png create mode 100644 icons/src/vi/nettruyen/icon.png create mode 100644 icons/src/vi/truyenchu/icon.png create mode 100644 plugins/arabic/.gitkeep delete mode 100644 plugins/arabic/kolnovel.ts rename plugins/chinese/{69shu.ts => 69shu.broken.ts} (100%) rename plugins/chinese/{69xinshu.ts => 69xinshu.broken.ts} (100%) rename plugins/chinese/{linovelib.ts => linovelib.broken.ts} (100%) delete mode 100644 plugins/english/LightNovelReader.broken.ts delete mode 100644 plugins/english/NovelFullMe.ts rename plugins/english/{comrademao.ts => comrademao.broken.ts} (100%) delete mode 100644 plugins/english/freenovelupdates.broken.ts delete mode 100644 plugins/english/genesistl.ts delete mode 100644 plugins/english/lnmtl.broken.ts delete mode 100644 plugins/english/nobleMTL.ts create mode 100644 plugins/english/novelbuddy.ts delete mode 100644 plugins/english/novelforest.broken.ts delete mode 100644 plugins/english/pandaMTL.ts create mode 100644 plugins/english/readlitenovel.ts create mode 100644 plugins/english/wtrlab.ts rename plugins/english/{wuxiaworld.ts => wuxiaworld.broken.ts} (100%) rename plugins/indonesian/{indowebnovel.ts => indowebnovel.broken.ts} (100%) rename plugins/indonesian/{novelringan.ts => novelringan.broken.ts} (100%) rename plugins/indonesian/{sakuranovel.ts => sakuranovel.broken.ts} (100%) rename plugins/japanese/{Syosetu.ts => Syosetu.broken.ts} (100%) rename plugins/korean/{Agitoon.ts => Agitoon.broken.ts} (100%) rename plugins/portuguese/{lightnovelbrasil.ts => lightnovelbrasil.broken.ts} (100%) rename plugins/spanish/{hasutl.ts => hasutl.broken.ts} (100%) rename plugins/spanish/{novelasligera.ts => novelasligera.broken.ts} (100%) rename plugins/spanish/{novelawuxia.ts => novelawuxia.broken.ts} (100%) rename plugins/spanish/{oasistranslations.ts => oasistranslations.broken.ts} (100%) rename plugins/spanish/{skynovels.ts => skynovels.broken.ts} (100%) rename plugins/spanish/{yukitls.ts => yukitls.broken.ts} (100%) rename plugins/turkish/{epiknovel.ts => epiknovel.broken.ts} (100%) rename plugins/turkish/{lshnovel.ts => lshnovel.broken.ts} (100%) delete mode 100644 plugins/vietnamese/Truyenconect.broken.ts create mode 100644 plugins/vietnamese/Truyenconect.ts delete mode 100644 plugins/vietnamese/lightnovelvn.broken.ts create mode 100644 plugins/vietnamese/lightnovelvn.ts create mode 100644 plugins/vietnamese/nettruyen.ts create mode 100644 plugins/vietnamese/truyenchu.ts create mode 100644 scripts/multisrc/hotnovelpub/filter_refresh.ts create mode 100644 scripts/multisrc/hotnovelpub/generator.ts create mode 100644 scripts/multisrc/hotnovelpub/sources.json create mode 100644 scripts/multisrc/hotnovelpub/template.ts create mode 100644 scripts/multisrc/lightnovelwp/README.md create mode 100644 scripts/multisrc/lightnovelwp/filters/allnovelread.json create mode 100644 scripts/multisrc/lightnovelwp/filters/bacalightnovel.json create mode 100644 scripts/multisrc/lightnovelwp/filters/centralnovel.json create mode 100644 scripts/multisrc/lightnovelwp/filters/daotranslate.json create mode 100644 scripts/multisrc/lightnovelwp/filters/genesistls.json create mode 100644 scripts/multisrc/lightnovelwp/filters/knoxt.json create mode 100644 scripts/multisrc/lightnovelwp/filters/kolnovel.json create mode 100644 scripts/multisrc/lightnovelwp/filters/lightnovelfr.json create mode 100644 scripts/multisrc/lightnovelwp/filters/noblemtl.json create mode 100644 scripts/multisrc/lightnovelwp/filters/novelsknight.json create mode 100644 scripts/multisrc/lightnovelwp/filters/novelsparadise.json create mode 100644 scripts/multisrc/lightnovelwp/filters/noveltr.json create mode 100644 scripts/multisrc/lightnovelwp/filters/pandamtl.json create mode 100644 scripts/multisrc/lightnovelwp/filters/universalnovel.json create mode 100644 scripts/multisrc/lightnovelwp/generator.ts create mode 100644 scripts/multisrc/lightnovelwp/get_filters.ts create mode 100644 scripts/multisrc/lightnovelwp/sources.json create mode 100644 scripts/multisrc/lightnovelwp/template.ts delete mode 100644 scripts/multisrc/wpmangastream/generator.ts delete mode 100644 scripts/multisrc/wpmangastream/sources.json delete mode 100644 scripts/multisrc/wpmangastream/template.ts create mode 100644 scripts/terser.ts delete mode 100644 test_web/static/js/index_old.js diff --git a/docs/plugin-template.ts b/docs/plugin-template.ts index 5d61a22..2f63dc1 100644 --- a/docs/plugin-template.ts +++ b/docs/plugin-template.ts @@ -11,7 +11,7 @@ class TemplatePlugin implements Plugin.PluginBase { id = ""; name = ""; icon = ""; - site = ""; + site = "https://example.com"; version = "1.0.0"; filters: Filters | undefined = undefined; @@ -27,14 +27,15 @@ class TemplatePlugin implements Plugin.PluginBase { /** Add your fetching code here */ novels.push({ name: "Novel1", - url: "example.com/novel1", + path: "/novels/1", cover: defaultCover, }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { + async parseNovel(novelPath: string): Promise { const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, + name: 'Untitled' }; // TODO: get here data from the site and @@ -55,7 +56,7 @@ class TemplatePlugin implements Plugin.PluginBase { // TODO: add each chapter to the list using const chapter: Plugin.ChapterItem = { name: "", - url: "", + path: "", releaseTime: "", chapterNumber: 0, }; @@ -64,7 +65,7 @@ class TemplatePlugin implements Plugin.PluginBase { novel.chapters = chapters; return novel; } - async parseChapter(chapterUrl: string): Promise { + async parseChapter(chapterPath: string): Promise { // parse chapter text here const chapterText = ""; return chapterText; diff --git a/icons/multisrc/hotnovelpub/icons/eznovels.png b/icons/multisrc/hotnovelpub/icons/eznovels.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8cc6d22b096594b94e045f99cd92292cf2b210 GIT binary patch literal 59390 zcmcdyWm_CP)5YE0wLo!qcPQ>!+!t$cUEJNJK=BrLDDLjE6o=w2i_7kNKY!x=kmS1Z zDKnFqGbb~N)l^r+L?c6kfq}tPR+7{HZw37~UQv+#o3|4GSi`_zI62D7YAVah(rA8n zvvYK|g@N(?k!gTzs53!SXris`0E;J5FN!iv@o)}~?uD(^Kx;h8 z-+4XUkv2H1u{yT69!?_N3@9vBSe|znDsv9ou@QOr&+t!uQ(ffqWRrUK46BmI(CQPki0=|G zc-m3)-Z*`%5;B;ZPxSc!n3cs*$d7uL%z#=*j0e*aB~8&!e2aj?YE7M0B&iiUMcWqrs2yV| z7LxfwY5%zM%!~Zu*L@*fGg)muPHXjtFIn&b$~FVddKViB!8Q?cpfi#S;{h5Dnf$>o z&B#Iemi>vBQQrsBO)d?S>Egu(SlPT^rnC)Tl5du402WO^4FwBS(t$b%yvhq63 z_Kk$ts>5WS`5&As-f$ESC&8pF{)zy(Ek6Eo@BAwpX>={9Xn*4MgC$5@^MJ8=<=stA zrY?_qi4BjTTt+qWZ>Q!DJorY&VUD}a;m@rz<8-)8cKr$!N}8i?nBT%Q@u+J4jiEK$ zSfV<;r3Jo7_5M{z%^@!xeM6H|#;tm_sC@2*JX>=7wJnY)5bLMUNKT>}m#4u%)jLcQ zC;pO#zamCmhB$y))heWyM_k!9#vbXn_oatnnCfm&?!j2B9L80hP_mGLJw`=)B$oK_ zhHhW?c6UmOZ#M*|9|28>XFx6mez^84=GU8iCq|5$5DXi5T6cW_l789aoYF2oEeSI2+zY46zPlb2vYcZi` zu5B@)3^3G8^5;i|rL0UinzGGSQ0vk|vs0avP`Q-fjSzmhj_p5ZE!cF-A}kq3gFzqCQBMyq4|u7U8m*6@ zp2TZl6b%yljwd)6xgTn1ZoX6O);VqsbMX*02Hh1b5J!ih#DtlpJYcnnWTC9<#@X$Q zAQE#_=*KXG4ai9*zkv&;L5T=c!a+}yA|t`BW(#{WMPdv;wZQs}RBl0Ph{@8A)IjQt z+|@5J4}%qszl+gKnQi2eG$JK_Spva)%SRr`(f3xn$in|j+)I||k)c>}g z4d4naWZdbV@j43P9a}=g&)#*7H>*%i- zshHYusqq7pKBeJK#2#qGG2AL4mRT(tFRCwcFA{1apTZl3JF9!sN2e}L;N6*iC-p;q z`TTZ{=l*GYTi+)+GNt|KwMF zS=k}&Aw_yK*&1$>Qd8^XQQ^dBGHS<8K1H%^TMZTB?(B0V9! z5oWTI%0MaT&xv`@g##xvK6^)fClJ7FA+TJ2p=rSikhsdS$g;Fv@1z7vD!uL|7(znr$#?o)yw>J9^ zqm_wWQD>MB*%`$v>?-yuw3k{dnAEzaF{Z<&zfbGu zQu3ek+2z)T4qBzlSIgIz7uYr(noezHJ=Olw zXRBtb*3+NS-vS+hN^2@jR*iQWi~lw_%LxFSXV<&iTbtEdzglcqylu|TCjks>3;;fJ zg|5^Nk`uk_kUaQ>(8HAThS|L)`m>YM&);i2yAL8n{Dfl*K*Ft<^cY}LMpAw8iwfSE z+)L9F)ARTFk$#4C+d7r-sp>t@&{8xfb+Grdfq}-V*eZLSkK##E3^` zBT@703r=8IklhQ#?f+eV=I`W$>KhdLlv6CeK}CU%BKQFdY~G!%IVN zVJ#K#Q=W#iLF7g&MK6WM0Wimkmgx@ESopnLks~O6`L8=5pxAo z4XYL<2*-{zi&CE)M0rIj!T#=xKugDB& zJ>HcpGAWLt_4{%UNj{0Psw`W~;b>h#54An9AoG=`7`FsRFAv0VFec&@)gGTT79Y>& zQ?Ck|x^bST;F^R{>gJDpK8=g5IJVIYLX@st5s5m%)1B^oxSYr+veX*mp?&% zzx}$v6ECDM$PArnrW3zo?eHsAozq2>G8|s`|-QLs@GNAAK;8i!7AU5C*j9jZ+iqj&v> z8wMvepN(&Q4}WDoROeKG(f?@mA3OuwQrVJ&G*>@^TCx?h1?yVPx|}bM(X>V$CU+;d z^ceNxtG4U$Yr2{w9D=rxmN3i7C0q#Hx?7n8b%I)5yw>dg9SzQCw3j&xzoDOJ=hcpy zsI&*KF|L7n#Vy}{lU?WKQ8J2Vfl6BA)@KW>eayBAo2naU|F|x>hHsSz^iz!#5(n@; zZvQ22t@o1z0JrJ|`t&I+C_evIJgWrx2*!}DlP{5NU@1^JyWRmr+{X{++lGjuS(8tw zjr=qGJs}Id*79TiNh--c1>+L89ey(4t@Kh2Gf_fd)r05qsfYdG><_UiiGYhA&_6@R zL37mI{hjmf3+Glr@%B@pJ7Pf5`Lh*c;KZBuzHQNznhX6#k{N3E_kT{~E4-^6s~l#@ zf405pUq);5t=-yo@HYK}MXw*vMI(LuuV;G|I}E;?zXh+srz5ZQiN5!CZB>7a52Zx2 zC*x6X1x*9r?_%F4j+0(APBo?qb0w?&t{&gr?G29)iLK2%yEA$(E?FS5$H`R&kM+SH zyeD&6R|{K|^|tkI+ibyrd+!Z%Dj@&U@Pn9dHni?3Xaczudqc8GqVOZ*_0;J4V6JTL z(43-|FDUWVzn-(NUSl%-_SGDbOK84^4QAUJKBN!{2L91ynY+rY!5l{qx}Mw*9IOg% zH1CG_6@Gnv`}OAXh!G~Bt^X1I4lrQGpM)iqhAp+Vg&b{sV07im!a;VSgM$6~x;l`&OxP99pw-U}0i4qW%xXGFOlDoSTQziMqF-CnYMH-6yd<)^k zJ5LGn9xE~Uk9?rID;asfzz{L~Z@?;Rf4YHz*$`2dlh*Y$yZl(IxAn`~iM|ZD7WPRH zrlW6es_Lq!v-4@5V9a;MN>j}%LG`7DVP_U8zF@B~VaXr!fZFbH6 zdZ*#2HTb+ec*y;wGoqgXfrHi^HB<$~RAv_sE*q!g|7*)2w12XC^Zs2N`rajJ__DPF z{eZ5ASIz|o7>XAaU=N*6zPJYldW(sQlj<*@oVrW=D6Bo%$;@1`6cQzm%3MibN`Gih z2lIb5rPQCDHPAOux331gH9tIjJgmJolNeCx~ElHJ~rBQ-pkWSknHua_Dwp!rj}SvCW8(y8P6`R|Oi|97VXr zi&5Q6XGidMDtu*BYx$#)>IcQuc2|(?c`}3?_)-6{0sZKE5QJ=JBWh;v|EI%qDT_xD z!KWVk>gIfKZFOIufI0aQ*o!+eM^?4TJCJSblYpos`RugeO#92UmynbEbh+sLx0wWc z1ruD^!UET;%??Ow=|@lf#}M$9bNlTLdT$RmUt zw(i;J<(&|CV9y{TFn0gjop~a%UTRa?U{?uRQ+OhkX4pgiQTj3cam0 zf31eT*TVeGcE(fT27z8dvMsZzEui(^^PfkBO!DLPSe&H!N@sdKiVdC$4J&U&*UvUp z36Yk75ds8rA-`*8^#6GCi21#qkOQChqD}fi5X~$|uPB`k$8A z5^*NQvF6hKp_hf*0npm*0A}v6VDJq#MB_R}xUQ&(@)Cu_Dg{g_4L~QUrEB=~lM5SU zA10!WI>RYdS9>lf+JM5oee|lvj_D-*t&Bf}-Bes;?QN>^ZLYUml!Np&|3Zc4_^d?|$|`g8LP~~-j8Tm}I(e*TEs;Rz!8IM_J;i!G##Fg_ z-(H;ThG{MSvLN}c1AI1zd_X_eCzf{B<@$4&hS}ieOpE4lrE^w~(dyBpJV+F&JFroV z2qp?8e8vgcfp{q|q+3pc0u?VWxsb@z`=)XlfYqU4iHn{~5mjx$h4>5R$2~O!?&J)5 zoo^b~2+}ve(vSFBBgH0|@5W@6Va1bzx?f8+CDW7>=SzcRNiu_*kQ3s-2c@euNR>lD z{~{tGzMaaKT}DOK!=57|{CwYTKeV*d*;wjucDu)w4g;r<=YoK#{_6~r5 zEftY`S9Cvb*I#0Zw=y%AJ3tKY*x=-7!Cy|&B>KNr}_7?swB$HR?7`3r~MC`v{S_hzlokoGJ->U!cgF@EObQTu$AO~-Gp*Y3> zG_#+m;YDj_uVdtbz3eM~FRvxE4H=^{#JPW1ewclkTt{Xc8r&yS64O~$0kAN_sTj$Z z*@0^-k zUQU=6O+#`JgGQez&2t&atg2_7Qc*4fkPQTPIbsk$a`_zDH^%qWN$acA;_lT8-r}^= z&#o224z-OXy5HwNMx1rF%z&Rs#+jHnZQ%IQ0p8x)BR7AWymq-Zec9`Id+G%b6}?{= z6p7(C7Z;SxOzXB71|vl5N?P4~U)o`5DX6~!xQ=lxR`I1S{mJR1^Wp~k+D2k8kqR{S z9U^*zu+WwMu=gvXKiF=oEUqmp1gs5@YFN*tp0H21dUv)p-JY5tZg4KxWYMjEPsgv} zl5sgCw|p-cH_eULvg_R2Fn?RW$p@Yp1P6Ycfh6_tt`x8PJkEV~ppypBcgdF?;Db5z zCS7C1fG23YgJzI$?Gj{FI{3#eJDGoH)2~9Ave;jra8&3v4RKvuJMNG86>7U!XbxfE!Wy!ffHgF%&Tt=?o?4Wvx4AXt zB@m>I3ZYtEtWtA|HiQ?yIe3sAZ13!?7-7pj$21;D7`3`F>(@Lx z=!{6@$@)Upps0L)5;}F0lV`N(-Ap`)1{uVpPoGs~TR2$aq%?De-OTC-^<$36Y9l8* zW3X)#V1^wakCk5lk$W*bXo~=F5BP<_`lCeYzfc7CP!s>mAP2A~tV^Rj)eBf<9eSJv zxnEAs{zUMt$;R}<{DQP1TU~u_Z$Gy4V~V*i01Ud>1BLIMZuj!?kB;O`z%ofS=^oq> zBQ^-S7|6|Z*3*4sL%=)Knbd+I9k6M@8~>e*UU8di0id5FqMOdlcNSUox1bO}(9R)n zQ_~O2ZDk-JIyiHFaPSgH*X;`YpriRO@u;0U?wV|GbrVaIdqIi#n<*RLxVOnyX3Ya1 z^)wv1dZ6u6Q&VFi?&9K#L4Ll84{<#)EINr(_M!w#S5Z-R4%8&2J@821h^^3i!LNGH-3IFJS>6A+1Lwre1c}u9f zM^%GJVSgBAGRW!!F_wk;{6sUDh%~}K7JW`mu9_U|0(VuZz9q{?G!DOeP!a=q zjA;m2@@?&AcjC&uLT=Hc76+hgXe7q9G3oyKx8L<%>0SuNiu1qEisN)0`P5r@=b2iJ|IYNW zm+UP;(bm=$U2Ou+i8^2Z7T6~6ev$q*q#+5B!|?tzBd?xM7ydV;g*g)i7UlN)n!!%z zbjZrut9Y%AJY41`%AeJ&IW#jl+f04Dz!061Q00%XKY03_&Gn0;oeTjktwKm1d+6A_ zM45+vwJwm8$*mpV>IjfF#SOZ@1M}#*;m+UHBZWskJ4b4rp=YKUW`~h65#02Tg=8R) zSkR;I^I?6r=w`R~)*2%9R)E<1Zg>$^a-kTg$JsZ*+?Fhd$8V2Y_?6YIKv&#s17cA( zk$RLF^OLbN3Q5MJ@h_l%boJA_R8kR-p86*{tVhCxl-R_Hp4Qq$6a>1zB2C^UP7;d{ ztkr7ENXxl-_eZbv1eLVw+r9M$pEik$8aHu&ojm`hE&N5|=OrTe`p8Z7IB6gA1!!@nSxOaN9lzfFvz0*~x<|o(RBCXB55v@LWS#E0s za8@GrqDvf0#eEdMk1FNF|GVwZ#>#_8>rLm4Rp0E_DB%AuQ(uO?r{A+0AUagK)x7s6 z*(12w*C$DFNr?$CR_+e=@_5d4>tza+$J&hz+638PMdfHMxO#)whG*?ehR!MwHs8+# zC0^EifXAK>XN6&{`BuFy;tjN;A*gjHu#W>Sj#|*HoP$m{s{D1g_TtB*w8H#O?@R37 zw;tB@0~SW%t`d-sXL8O_pbp~+(k&iQr|3LsX%vuUJPgJi#{RK-wf^E=@$A2FKN;NM z6LPw0HV^h5xlO%;_ae4s<2osHT==X0S_N_?n08mE^+WRJtUz3tAZ)k31?7fmYjos; zZc?b*>cjU0u0=YLP0N4VtwxQ#f%l+qoIs(#*Jq`C$u(3q*zzWnr`G;p;8XOF+s7*> zrum=Wo01Fy?fs(Gt6U8DTtu{1{A!Rrt}dRM%T3C{A9YY{>frm6z5s!XD!BPc)j%$M z_*s4|FP!QjdV)f$#xJaCh;Wb}omrEn4Dys8Gu8|i1;~@D&QX^MdE@(m)?3o1P!5du zsFE|4r)O=lDV+8^CmOqInZxabxG4w-Lq}ob300OLjRL{|9|BPCQ;v_EVjj%=(b>aE zxxDQu)%XB6a#Z?CftiK%*WSywgXjI~`osPCrm&?8CJol^kK!k`%b_S-|5qr8c45W{= zf&NDJLIRA#ZWmqfOQo7InYg3j3(NF#{ypry77BUtvm71*0SZ}KY&3l zgZXaLPa1_knw-p(+eh^D@%C6y^k~1ml+M{GuwDXOOf0{@s7=No8&ZxNW;#BdmPQXn z7m1P9)tFPa>4FF4E);C&Msa6BRINe3033|XbO5!yIR-A6PWohhVOx-*t8r`7k_83X zrLXmn!#V?)@i26g8>!nqbyvT|^k|umzCd^k_xkQ~ovbXEozOLV-N1D4b~~PKFd_9< zp=I~rww_@%#W#9*rcS!(&g>I6UaQLhKJ5$lefPCf|1PtdA@HB00?tVQ%!w<)&Yv){0$FDWkd$JP%05Y5q`2oyJdeUFrwWv z-TH93ce@mg=1#%qF+l zrf+EK7uC*BspnG&daPlm37e{_4%(b5_9tz7Ymnb~0hY>it~@gb_<#PT-wxTM7%#|% z;mqoMa;6S^F5-q2+E*GCzpgDH@xT_8uHH=l^B)eszyGYh{j}t}6vuB0j20|v_TR=6 z<1BAPn{8E3*q3EMd9pYO|;IZ@j#2GCW}f}*!t4Q z^KVB)$6a8<*FUT$BJS~B*1<*p4_9&)W0Hm;!6?&UO<1Z$SPrOio22SYCFz~^hYcPa z;A6lyCqfPp(|)T6!3>Vnil8KVGzt#S`K35aCjb!xF>#4e{*>9oO@TIHo_^*i!z|td zO%KveloWL+(>8RlaIpcl#g{@oY>z1RC;JaC!o{(A6vjuySGv#Bbve_vxZ{IR)4Db$JK4iNw_b}$IJDNnG6S=2W# z_YyiBNaRKV@t1?te}bd-v{kLfD`|Y-usMN%nZN~iobb^_W{4YRMYa`NJk5p!h!?g+ z2%Rr)f@S^i(mSKrj+>;m!jp_?!#IzDj0AyDO;2;8BASxZW6#8%_3t=)(*qpW&5eN7 zRQh~9V{hGk{Yq|Dt|5x*#cZFBd(;O2g#74nubi{Ft}E@xmtRiwt>`WgkGh=`p;Gu> z;#K^T;k-SPf5N%fhHZ_=Cch+?>0INGG4eNw>9z5!)E|B9WuG^zdvh;wW3E2_%@(dy z2GS{a-48`r6Lyj-!-!q#{!At7ARCU@?mxvnbm!!ggF@N@T__BuBw+VYmwz5|UqH$G z>KpB-&pi42e{-@;`8qF{I^yQwlG^6onz|IUq{1_8kUnJr3}8GaC$(VX5$(3Bc{*3! zE*zj%ga-pa9^B?_lUF7r85|@yMRgYmcLaKP)}86=QG974NQsLWBVZH-HmB?3z*XRf zk(?!MisP#S&&5nf9bHRCJ=C@$_CGH299E;}_5kzpI$H9HyZeZ+?sv{~?Z#@WI5dAK zk$g2uQKJZB=L3SBA$=oPq13j&+LUOSfnvnUT2t0a|6=G?z$}!Xt~dK`+lv~;$o)nW zs9^{H*#9^>>i#iFDF6OA%w?i}qfO+&9O2ASPO!cP+21<~*cc*Y5UH=DT5nM?;!|n| z*pT$g#}hhm?W-O5!9pU(xYrFf7gf{0!!`<~eJeUL2}m@aayD*Lt&KO?IJHw$hz;qD zOlbX4)<8(pB=t|<;jd7$l-l#(icrYac4r;JX4g=Fpc}@{``O!&U>~H_(uADi+s`rh z-ba^Uv=;+e z?GM%q4(TZKaJ6V!@yfWXz4X1;$H|*SpL%T3M_n3S7*d^uiqd)o`Nf^*iL>g$Tazul z_iS^MomS?wHyx#c_-Fp3MFp+ck8$7@OPLZxqMV8+58V{TL`u zj$tG|w<($~o*6ns@+<)8JnZS(7;5hgmY;5AZ9i?CbghO=t%bI%*2UeXxO@|Ne_h=n z?ez}`$SI&?)KhH?0Wam!x1#oT>F}yM9SX|oLCvD;=b|K^L@pZpy=^6kT*G=CYPhs{ zcZVHjGKj;e4TI+%B!4e*3U_tC-emD0qW`;<#w^8=?Z}vSa&|Au)e{I~;bRUA6m$Mk z+Sm&F)*blZS~p*S<3gv<1lS(T-USGgJ@yooFn^C_j;#_=(c#WTjEc!ma7Lj#7*KdMwUuJwMf?wdXcC1sanv%>MCG}bQi zBV60P*>?lpwnzZ_7?SE7no-yt~jdbSPYQygjt5g{J0v{ zAJCv77pU7EKBD3Tz<69&{f3DN)kFI8(RSllBgS;|GDwmVBSbONf9h}8Px3J2*xby9 z7x-CWgp<70by#+cX#adc7a+Tli|Vbn!w+reK(%0|R!N)wt?--4nmcDiQquyt1BVtu zKO7uLhhJW{xE@&XkgI0UKT}SVKWutz#p0uOoa6gVhs0Mjs0LXBvOGDlb~$gYY2n=% zi@oM4?<1<-GFv1h-Wh zn#4$lf?A|;`lYx2IYQdF^lodDJPH+EJ#`P{yC$Jsb0DlhtyqFXUQ3M`+rHnLG3w-* zkCKp`LlFUFKMiB8{6`Vnm_)#|{Rk2MbZt6-e#*WU{MbwyDt*4L@tRh9U;`MZd|q7X z>4GBn_4)hzch-SxL30|(APo&H(42a*;2f4lyVu3(5qh!8;_7e|s0DcXAJcOh>L86IjkehbAMe1n@}Z{oq$I4m!V`pv zrSNd_F{WsPWbEX+BFfY2=leJmSCv-s7TU9e$tTG6&W1js31#MYfB79B-9TMl5tG%l zdXh`I!>w6CcZLz&YNo|c54~o5#;4%DJHEo9dapnMN-(Jh8O2w03{Lr?z=% z=#P5Vj)bsSHw*jshZA3OjBmaVazJ|~rVab(vDINQ1NEe2H%K$|4SG3yws4AR%Ws+< z?`vDJ6I7GJguN7ExS-|ghHXX>7+YQ@0ZHa)=yrM-_ z?xz%?R!7miHQ%SWZt;1>uJ|8<$`q3hwN!JP(XCeQJHfZwVsIltiO2oAFyWwN}Gw`g^7!k|C5 zTJ6V%MoZs-r%cc_$>;jQONo!OtNpRQ;7*|L@x{sWDD}q}^p>4_ac0Syd+ueglf*$* zwXs%CcBkvDEOFAFp8j?kglFXd0D{&&WBwv}sxxZ(YNKJgnAXiJNPx1dkO3J_{OP7b*|NqZxUE^25i@tVdt8e{fLuGXEP_wsNxzbbNg~Xs*OuryCPU`{_`S2O z5L?y%3|YOpug)<`@ZwQ2!7`qs1hwIjdQv|)D-fqOOCtvo&VQ>wL(Q#K$m2#MEIqYT zTb)D3=59Ye3k@aRj++Uq%UVCSc9$KC#uHN+=D(6FU|1PHs0oABV=Qw@yn3e&Y zEh&ef-5Mua$EEyK@bxIM!XS5PH@?KN0v9p`*5zk@rP}2{Ok<*UV~oGbt$%g(^no zmX>A&|4#^HcY0TNzQ$<&+OCwWXI(_Qj5BP1`!k9h-|79o_vF=ljwpg%9rEn$Y8y~Y zxZ5b+nvN$vyDkD@55p1l+nmr5+INbHz=M>|kNuUsCFHJ0lX~-9$2}5h52wrIN zzHNyY#|rL!Iu=FbUdmH;Oty+>;+`aA0gq|7+j@T<%xhggGnWXE(0j1zW9tz~(wCv* z<2}tuG0BwO!jG$t0K zcKvEX8T^rdjV;)gYi{f8%Z1DH@@xtPjgp47rbxrTPgITf0+m=qHs+E|MM01gYj29SymH65wM-*MwBE zI%2~iEg>+~uyu81j7SD?^yZ)bQK4o1) z<+HTeLq#^KFgz=lEpQs-+=tcR=Kx*=(X9C<@X>VP=@hrDuQ+03WiLFL8vCiR5IY(B zXec@ORTo*E!nbxFN&iI&c6Q`tBLF}hcMJ9)guA<>-yO;>?0W;cLI^_|?Z^@3`7Y(( ziJwOIIKZC8+3`0_o$rwx)xILY!TLkec-?`L5kBn%Z7$n^RXtq>STCoTRDvItXpW&I zlB_w^&@O!>h@U$7t@`SnfhtC8!C(`0pF=3|e|E_jX<{z-94XVBxVy>8CzZL_7gSm2 zd9F%EIY>pL|C4=EM3aQ6*y>f106luC*hehCPCdRS%se2%zq+g@IXbc z2;u(6#ca;YNvEi!P)AGZc^0_1n-VFQKBf~6Lo9GpNy0FEN~W1uO-Ya_*d5n0K?!XF2Zb5GJyJJ}B@^Q^RL^2! zL))qo+%(Vu10A(iy>}3}lQtnu8vL&{fI;1YYj)?d9EyJIk$7{w=jjv<4SX;>1-N}} zdRRa`>vv5h$9W-Kt(PQ6{un!o*~1G6%KaJ513@IzfiMR|v zEDpAk{$fu-O9OS=Jn~rW06Z3;a6c_zKs4V-ORw1f#nh)lh%x84aq@2i)uHmavsg^z z;kIzek}#fAus=!W!C{#GBl9WzP-|tKD{rWu(Rktqu}(17yQ1&+&ok>A21QV{k}yR^A(<_grDW3e$QHcu z%D6L2tWHm+Vnflx*bLIl(&j{>{by28(ofMy{ygq@KALd?IKsP*t;J4T7vc4h9ZI%| z^^6f`;RDo?7Vz@kp&Wuq2{>T7MtN}Y%X{fsB9yUE)NiO5FJ=LZT?tgC5d?3Z;^aE&- zyH7E=8NXMiH9x;8){#2%Okl9vt}A1y1vq=g!=}R|P(MeO=v2)prW~1&&K1Sl9p8U(weUH#|A9?%aEg zW>j?`Ry6l?3|S-3*~~M75MbV|7m?3O%Q?*y0pR9`L?b(W&SizXtJS)ANC5 zd!xphH4FI~v2eN@UGo>=1RPN>QD*_>eYXk(OBR~7Y_N??%-XQ979dA>;K1=|E&i>& zq5*^XtBww<$g;0t)k%XGhWWI!2MsA}6e@Vw&30>ikg?c{x~6VUf~V2~H(l+CWb;A2 zp|S=EIqI*Hlu2jyqQhDb2H3q;XjT>rzY_tz;WI_RM$CrBm8zW0SJ>nKjwtg#7QLH8uS{jv=D*Lhp%F9h zz4(IVVrK^TI_qW*8EZ}hIWVD~-X-iyopsK<0$*uTG+G@so2U0+(sHZqt8l#OIuUg% z0nID;m)m=uYDRxWb^#v2EscL3oZA|2pLd`>(A6#zQ3K#pd+_<(M?fEB19%9J{|M;a z4rz=MPdN(Y4yUS-9!6YFJEJ+0BL`cEW=o_O?dnPsKNo(o;2bBDXYWp=ZN&sel!h^Y zRWyHWc3Ax$eI+SYk@W^eS~6^(-DAxvfQqW*wek{S(AB<#VUn|KH#N`1@+_F1j_r&> zh*$r-2PCHs1XVy$3YsE#-&wl~1=sQHN<6A72kF)vK{W}SG2 z)4}Ym)kGLtIf0h=T4#-@CN$}7pym3$8a z>ExZ%#EG%#_kd60%xs3CnkCZ5=1Ubf9AmCDR&WRE*o1OTM8tF4hI)uH0(7E5gch?RC%g3Og(IQ$~%Cv3ArPK}#U-+ zd2@^wE=V-p2r{4{K^gqLTT<-nAH0E+JB`G4t2;Vp{2w!OTz;%&c%;H8p^Yu`=m+8= zt3qWOb<5Yfr(flDwCLq0t?Sk)E0d`(&NuL!tiE}tnv4^r1X*F;>!G5EM^VM9z1}^D z+t~EO>44_eEy$C?-DZV~FatYXe#@aQl?+<3J;KdKQGe`Fe>_OOhD*MmJ3T;gVR~Kx zj-%Rh>QkWvw9a;IcWa|VW3>Mz6O!t!xSw50Y&~&(S8YK#NJv z(>1O!cYZ1C|><;ialAPJyLC*lQKzB*V^uTBCFVnh(J zmWWT3QVJx=>!`$*zkIb#;yWhFP%7i-8VxN074BW+gvCV|l z(#sRmibrS4{C2D)iMzJd);&tSW+uzOO0LCuHH*1&@>%fm4fG6I1oKEZXFn^Gp)|uED8^qtb{0lVlZ+GRPb9SYPpyJ?|ng zvl*|24_n(vw1ZSPLTBsh-%BFT7ON?-+Dhx9Lm(KrB8|NM3;8e?Cc24-41&{zx~(+f z+e}*adC1PGrFQibE(cd4KP@ueKkNn9(NHMKY6Hq|%hmK++}7CHLq``e6f*J9ifY`b z`~2PXk9j+R_5x4^|9SoknYJVR50{y9f{CvjSj3T*l>~#R1o=`onS3l{WoX+hqnN|0UMwMTCJ>2_q9< zur}Bo2@b-jHu%*WyXHmc2R{aZH4kH9_oufuB`MV*uX5+wP^L=@l=O(_0X2{$liKxz z_^9rkRfg-8kdT_0^$oTPQ$=33ax{(F8;7^;+3MNU81zg7IT^bvAVBbJVb$LxuTfV_ z`_M=k!saFUyGRa=^^6=9FLoSBK>}ax`}md;hl^ebhcNhkq&ue7481!z*~lS^LhvQC zt>*U<5GVaJG3o8MAM~+Z!RFIlcOEA@5bu)jg?ilX7{6)$BA6NfJAR<1XZ2wAN`U~# z;}|N!^%-_$0bT%ZSya}%kXn(=%Z3C-qG$8Kmu1|1dwn|6Kd;}PWwEa=YOt@*{_c>I zw;z0|Vf_SW#XKR0m9V*X`6-md8)-e zDI7x##bHakOh?2HyWAH9N#uhf@B=0Zpx6Esm!)}`CESrN19v~Q3F((ksCGGO3Y#H$ zVTJ|+_azZ?cA4t(f3+kc#S-R84A!h3^Ol5r+OonSxV&MNvuq^&Zow-4pW?8qN*WEjVHsI_Tb+(QIyuOYaDpjf-=k*P4Y;#*w z@UC;a?u*jj{0+KHtSyE!f#>!MEp2N^bNOCImfo*nolD93^S-%xZ|F@@`I!n3ZHrjQ zE^^c0PyEE%FKKNS*xJi-#~jKf-Eiflc$$4dGd$oM@^XHgnO>dkUt4oZVTBF%lW2s` zk}$<&mQ;yhV36k3U_+VD*3(gLH9AdEQ|~h!^pFr=h86;On2%WemY z-vZaQEoxiAvO^D++E?go+c2yP1;(4_2`j*rrjo1Y_7xj2<_o5tEvgZ|ni{A*%U)i~ z$WD{Sm!h8TknJcXYLedihFakMvOUl*TQ%{WE)E4E*xn0lSZebmBJ%w~o>p6Kuj&L# zZgdF-etxz`$YERB~W<*2DK@3_ym97)eqqB00%FXTWw5 zDa+%Z)}no2EaJtuRi)L)-QPGr8KI~{_dVp-M+>(JbpC6V z%SH-O%Bw~H5h=u5H%9vLyav-C|dhhNt6 z4#AjxuQ{}N#Nhg5_Qfc-C5!l8P;|*S%g5Zqf)d#zf~-J(aHh2DI2P2&A>OE?z<=(UI5i~T=2zS20``Od?{w6la_^C9;H1M8h^~-0~ zF)VfzjJX&Xl#MipY;J8DfwAj03uQhpt7nOr!eU{US$wz(Ej(2F6>M>_ZLEN!a+Ieh5K;i@ z>hT-4;xfX(S5%mMazv_|3H`j7btmA(&d?1g1&hv87`FZEocK;3@Qhp9lSrpZ$w4Pk zdq;7I3VpaUHin)5U%)qT0=()?@(|Om-Ysee-a-*8e;MNZFNlNou~xK z5e<-S5WdN}HDJ7%WqRb8o;vPZg*JM?(AiKCrhULV`_-GJR9q-RKUN|yQ1AY%Rg>3G z6X&Uj^W@F{InbYMfUpknJ|K_0XjW5Uq0Rl_N&UC-o5Y1Y1V_wUEqboX zI=KJogW+HObQm72Jg#StY8>Y&u3DM}@HLaLux#dOwW`S}G2VT4WuNHRdI zg_z=fSRU0**iv+=Wzbcf>9u{TgB-0w9z%d;}KI|%_=0i~;y*l^pUbPi10w6*wG;h!xXLv`n!hs}4d^S(4=aTJYB z^D-Sw;Lb-YvCNx^0AQEtU~?j>-s>U7gsp^<%p0J_NGjVXbe&g;pC;6HbZ+uQUe-VJkC#CcrACTCviLc@ zUVc7RG4*HquQ@+I{~oB}k^V_qa(xnM=Z8<84!{4w+$1Rm14~G3IE8 zf&zqT->xL6a5x^C=j|&Za3aMVm=|QJenz7c`H=dWnjUKFw9Cod2efa1u9 z$ehz%Qa6@lPACjR#XQ8OOb8x(I@)a1EN;uH(oEh`tKv*XR-WD?&-A`c)}JYwKRH2Oc>crkc>TXeTU@94=_~VsNQK>iI{Tw8ko5 ztTzOkv1M{|ViCitiLJ_~i$}%T$T}I~$~6U5PKIkzK}uzhNzCl%&H@bVl_k*}@89FE zJJQ%9s)WF*=BI*KnrG3cI6D~^sl}MBLnR>E*DdL&%y}L3r$q=QYAXZ&-s`hf3Oq0M ziac3(Sqis>3!>qYA}L=7%0eu=M?(Pxj0Q@*SEFq+d}-_!eKxbE!_o6AlP}aAq>M~OFQF-qDE$}Fj|_OR|H$~V^E+h-hWBmL*fYmW zCEXSyjb3T7S#ezV9|dj-_IM0c4bVsCKt|RQ)wVzDKg@RgAuSMF+Or$mk-b36Ldia5 z35yaSJ@0aVec#;4wHm!Bi6F?D%q(wWsNlwmc5Tei0Q{}5nU&U+t4Aqiw9K{`Z|%wX z@bvMgW^VH`aDtIX0;>Q+!?A=+P#>*ij98ck{>O)kJ36~(rp0h_XtSf;^w&O1nfiXB zKbJoXJkWGku3Y(~I(V(Or5Ov78p~XmQ3zt#3kVw9TmZ%(yoeDymdZgbkEi#La0ArJ z&{Juk=G#KE);7rq$Zr`%k(_VLFOFIkGp_las~twkeR)|#`W0U#PlsS52%hghChE)k zPxLv&4Xybo2uDfFc6F;4hp^cHc$7}{QPaw3o2L9*t@K&e)HvM`;adod&*aA+57%_0 z>ih595W&BbWe8~$jIZ{6J!?y@Fx`qzhhcd_P$S<@OoG3Ek@1yt^<`tiwd>&A6A=qkou7zeb83La{E2Ycxb_6}sKtq$8;+ru{7 zd=}x)c*r|c8wbk!eDe6|u&Z~Z+K#pi;h)Xd2_0M4me5%%irZm|4`AJ0FGfeu zqVNk1>F`)V7b;tM z{%ZSVN1IhOcaGk5K!a#x6^B(F|KY*?hr_;@%5H`FgTc=3-5+k1WNSo zLa@?+`zjK0N>i==8_7k3-0yLO%Er2P z+^u$P_m^*yEbOe&naOGnqw5oXaKpXnvPnC_h51KgwEl9hl`;ji)~Y7F830B~R3&pn z%2&W(j3#=V={C~{fj3h{)`ZE-Bld|I4@8Q`RRLTPcf5vRJ;Q&hs)QFDaa{EM;swKgza!td-M~}ovGFPwNkmX28 z2bpA#?hku1=?ML^b{2*pYPUBWeSADzzj1B2sw4U}8#57{A z`)9+=o41B5S9ipyi3*b-a3GEN@Zm$X`*>K@QBw#5ZP|ZXn0~Fdr5USvD!wiVsn|e) z*1vlVD`=bJP?emx_%v=yL+0GnF5K{yR8k58KuwKJQgteRC+14D<7gp)s z^A$wK{FvCz+94u1reHn8_0e^q4b#9BzA$-05Y>NCcn0Hnm8L7^=$agTK2+ewoRRV+ z{SBq81$tb?v|gJ-y!R}4){K=KCXE!>MRT^vN?VoSTQG_sq3@KJV{Ph8>Z(og&OYc3 zfFa20E1-%I(etNMqc!)mPa62weZ}0lBT>94TiuP;XX!(mOE)%m;-iaGA3u4bho!kT zb#S4G{fRV{hoXC(p`pgu1{6`D^|%KQ9T`-h!RFlK$4`c*ddBNjx`T`w#<;it%%j9a zeE9G|qM*{=D+=fD?Te|Tp?0M;9Xt?2U)kC|y?X8HV-Ni`UrRHV97J|E2f>Ca2yK#V zB9HH6Vl4m%0j*?WLp#wGM1Vx^AkEr@ozRHDLPC(}{@SJwnI_=Z)YVM3Al}Vhy6#2U zMcd77K`@cB3B$JL)%Ef<60L%(Gt0qvgf`xY(`U;rx`2o?}S$vvCPNntdo|_TQzBG zsTybubA(TDmVw2@&wUwX%<*VK!(R<%9Wduo?`UHw=GmdlFLrUm=uc$&9f;Tu$&e}d zZfdRT%FY#?2)ZHWkw%gh1Dt-jItGf2hZ++`{=gQTkO0zA^SUQ<8(Gm?l(Hk~1 z+IuSbYBdmo%SR*576r2$H^2n}sxur!VB+ZmQ6_TCY{W>icT~-$j7=brnqzY;Nli}> zI=)&07yhgDvyftZL6ZPsVwN*Y8ICg*K ztxdUYW$H9H=vG(2_4!|O_5N-$OxjO^8lr4G0bqe*i|%h?4VRh4fpI2}D4MiwPq-d3 zp$7?LJAH(bg#`AR;TYl4OKfgAJXfx#k!n0!NLZrPCPhO&5!B$4hnvF!(+FU!2{P(c z`;78SYGX&*W>YwSq@B$=2++*RZf-iB$@1Y8(A&3fyEX~_)cX3};h7lm=`&8E(cUtd zhzEOzN{iZ+Sx06?vT1JKyg6)$5qaIdb6e)+nsO6mZnCpoweTOPKN#y+fsGAqp}~LK zm~9O2z4Pwyc=zylb7Om{<9e57tkF1X;?ux=5L)>n#XCVG2(!HvVm0V3y#i#n)lR(4 z0kP?Uq?JOkyJ!235Duphp`M{4fbU;m8h_=VaB$IC*>t(rCZGN=84|sUiJ9N{KP&a* z5t<&%mE02wztBE}R|MA^sQUM=iRgJ-%46d3o&W--OlF(|3yS?m=13Y(#)UMuU~1Z| zvD)lyn7N7gi#Z4`!K4HiYHkK^;2(q#<GH$k{9~ z4VM#?YWR=K0o>u zIUTNQVVToGA3b_JJXLb_OpLjq1@GWOr`2q14V%gV%{CSt0VU?VAx5Jb*S!b#9LQf6 zqY`PH$!t85*}0|6|99TG<-F0=-J`=Tl|(aFzUFIb#xiajP!5kGyh>6v{_1mab5U(s z&6x)@i}p6rW?=4fq3+ArQ88%4Keq=7om>m#*#_^iHT*S~m}%6Y(72-jr=fvjGgk>C zz*NyfHxq!50BB`2vD^KMJ)W886csDxuN-?i&kt?dZ6yDG7tgRa;GK`|j0gp(#) zJJ{OV{Nr868i7~Rvt{onC0oh|_5qYhJ7l+*w-LkzQyqmo(sXo5`GwLAP#mrLF@3$IFF?ttV8CT`!FMzo*Ej?)eUo(`&9p16v#0f= zu)l$5txu3;SJ#c}YtmfOGA%?7l+U-nw`=pz_<&W#XoTqx6g50N+#hxht`1k^$C0LV zu3aER3=wwtUtXrlD!vz+pAa~ambiL;9iRVrzOnw=rO$xb@)`?V2#V%xq*9qjjRX7o z25ccBbQnT_;C-11g{k}-@|mE7^+TIGi6AAw5VN5T$Zl>iiy`ggnJj275UEYkzQJVW zJH6i0m(56EwAdDt)eOe7d1e$Mv~D$1Azja4B4lT{RV32nNgEXmAxMLx9kVMTap2Hp z2NuLeKx!8#BhGIbaCnYhWA0p-zwb#T&VA^QEhv3oEY)OXB=-`@UCFUVO|*FY#Y7Ul zIaCjHx$tb@tmtnsyf<=MJ7uqwxBHu^60BkZY+oGVRDyeWXAKL5m$_ zT`*JD#I9V~F(a{3xV^UJ3=Pw>!t)OmWjvI&Tid>Ry1upb$IDm_?0}fIw~fRmhlQ}V zMQo9&?dl?l@RSkiRq!I3RG}rX5XyGO09q$;Z;RHE(7<=wZ{2l`BTQ#XP5;S#8b{cY zI++F~y%}5if@XU^lDQ;K5ba_;WP)zt;HK+Ugxmf1{LKNaN5T;>G$*gDC}(ff7!_>E zIu>RijNxI7!p6pH34-1oJWcFk|D$3r`0;isqTSb6_MADPC1~$~QurFe(uOH}O|?bD zYeyQ>sS3TgmIbDk{-gGV_gHb=r!>UR%YG)-nxeVc--X}Wb}Y#PE@T5v_4~TE<{y0Y zgW-W{3$BO}2)WAy8*bdVF>K3(!{;J=zV~d;5yRcHXRZ%Pgis}>wN`atQxJgch*?;l zTGuAUCwc}(BwK^3$83dprZ$KMUe}t}e6HQoVl4UrV$XK>h9?gcm1Kb#_$a&M;6MjO zlm4^BYiW(8t<0lXkID~lO}f0}vzSO)(kILuor`2Gk0)9Lgwc^5#}G<|MBq2 zii)@}GZ8nDI2a`-aH?%qpc}I5=GOC3N80eloe?=KA(`ef3CG7 zlg)So&z6Z(TH^u$Mokr#Fg=<`O|#Zsf!EYvdapU~yiGRl3+2WSSr|a|-&*VC{in^b ze-nH$tNj%Q)f%&vd!$XkQv#s+uUL%6BSt|$)P0>kmPcx$iL@A&NihN(D@yfwGW_08 zo(vxxe`3a36N5Ncu;$+-5U8(L3xxj7IAH%284e`SYTJpo`B<=yvou^C?6I(EAwl~I zRWdA6nn-6_Q(>8$u%0B}?ahth=4~YiEA;L3B|QZ*+)}7~CjaM|!t2HrmW&Goi9v4O zu(oRRkT5%0_E)c6ao}Qo{h>qb2n%wTuI^kL?!5C3TSA7X^4-vUD@vuhBL8is%$W7P z`S3{Q;?3(fj=u6s-)@c2)cH#9OEcEUE=6l2$rbWUm|pnhP;GXF>Ic&JYkzud`#7+O zDb|#lQ=3SP7)iziS0m$1roi<%LDEt^gry(H{zIHukZgv}H9u1ijyq3VNSLY6Ott4~ z?7xWqxdFVq|3n>Gb4&lw5pa=@B}v?Q7fC{A)_WrArbE-U(5r^CX|ZKy zxZHcBh`5o_?tx|Zx3k?d5Z#8xiU80lAY!PU;ffG_O^mRr>~|-~k||$NRs|81x^&a) zSS%j%+Ll(kdQIs}*RR`*br}F%XHj`B#>XU_sf}B=Zp*CPamEE}UYLJ`=?RQ+Y_F-I z;7Le+edG2Z-)h*nar@ybanTEITAHzFtEFeiwB+3w;o^o6@|fvRS}p4(WeS-Gkc@`7 zTdAP1pYQAge=gPI7*J89RY%WQl>%k?|UeLniK_CHEL@bbpC3x;GQL>8g@~qjkqX z$A$hUU$gu7uG?@_UKSd~;Pg=arT>^u+|R#WRQ41jJjSz!4e-gEnS=oG>Skv+7q|C8mYZGZr zrr$J&oO#BHFJ#iMNqu1oQcHgA>|oga;0MFcp4`&`SQM;LprdO$8b%H8%*a}hF#rZf z^&)&Jl}%B#wK&4IG>9kyq?w|Y-qxb)J}rfecEcosgG#)K$_D==0WbrZF>UlVA1a-!NBOrBweo=fwlO#iCoT|9(obvSNy2sxnib@8812 z!GY2a#faA58Wc%g>qy{YOwYdp9x$PkoZ7GCn@qR6@-P0%*Sde5^9{&J@ZM{etM_UG-zfqECB^mDcR``VkPRz)2{cO_}V$ zQh$_4BLv^O-EFp#ZDSz~Q>uTpzpwSN^9`xPC0|Q3mdamkgp2{b0EQz_S}A5;vIcEk zV0rk|AJQ|zY^3RXQEv=?Mbdby2u+1TLM{THo3`K6-W_5o5wi5x+O0S+ES6Uxs@N{F z-|bXFt#38=>2l1Myux5+h8pKo6f+z;nYE0k9xaXP76PI$BI5*c4q6Z$%VBuwuKKrw zUk?FU?`4vtFMlBi)s2n%`O{u2D|s*tpOGUdFq1MovKMAMtHbT0nnwZK627e*|*MZ`z=3qz*U^o1M^<7Dzd|Pqc{q&M;h=i0Jh`EzMXVp;FUDPnjk&ZgVQOt4S5+ z&~G!Zg;0izU*hxdvc|D*RQ^$ElQE+LrZYXImwx$LA$7c71l|!d$4iRZxRz4+WkMD= zSc18MW~zE;JY|k7(|-$z-r5q_6qP$$Afq1p&xEFNc#+!x^!$@Q7w3vQr};0yrRQIe z$*GrFuw4@{Wa&t%%NOx3V8Z1Ps-<6b9o$%Rxqt=C5Dg!<4d(W!lM;k>u{Obf6$2$7 zJHZM{%3(%^ElT)snkiFhgS4J`P(Mbmkwu;O9O1Sq@CMxH7`-^`M@n0TT8Xvxw-HlOq(f`<~! zCi<9Y&6wl-8Gp%*m~?2!%-_C}#_{avVEEvZPlx;LU{+v)Vlrkd+w9z%0+|hs2)LNz zar)b%!W~I+?1@amJ_0FO8Oq3DZxtq*^7HU(j-)De>b92&_Q;x0SbRgJu5X_o40pex z^H2DlXrzh0-~v<2#KRZ#)|SGJO**3~0?T-7ZuACH{V(S7Ar}%v$1pL$48r(gyx=*h zStQ+JPIq|$v^g!x=NqBW(0JANr5USqV3~ z2~Zs2E7+B@KJjW1pMx6#+~}T6G!TH5AvxQL(zQt&<#;d9g3%JB&rsC#oPl#$8^R zlQaemEx_MvUCu29_Rvy^SovwJ$p)#?zJbk(tRa}R>oj>| zRFG(GVx4BFu=ic_d;+lc9`}cV^orf&yFVug}1L4HHoRRfIi^WkJrm9d+d+ z&K#&<^CK_q$0HSscZcZZhavO{umEVPgxPI0#QtP7mIb%*+Z0^DjW)|)PqyVUbl{MN zf!5N^v#=;`(=9~Q=3t!On1nRoUb8$CScZ8`4rLxSEqhE8@%i!cmpULY{+uXF?k@=( zc1%a1y?7^PSmDhG{vjk!MO#_U zHU}Nd5ObLAGSo)dq4f7;g9~y2dM*Mk_bCZ zFj$0t2^orpIxVgUqsG+dCMz#J^)q}>o}!-K!=*X14WBJo)HHM?RKmpN6w;dmGo}sR ztBlqXytOoA300Wjrl}BJI6hl7!UUb_lJIt<*cvPFdVm#sYYzJ{Az-KCAnlCcY6Re+ zo_#N-VkorDA(KrJZN^eW*8Q8#Voc_zA{t98Q&CK(MkE%2+Ye&H3?cF9gJoSzRPHU} z_Hi?1W@i382&FLoGDBTUJV~(-;D!E+kGOZ?*zdry=EHN@q!=@>O*U>Y>!coBaF78w z>X!;gq9Zx+@7?{%@VCDCYr}n2!Zk6Av0P%Bm&aQESw+vr-Pgu*L}zPG%h#G4^Pg#c zWG%2xnQ>%M*a0hiZe3mHOfEflLKcKF)<61G`GJmF;=$FJ7ff->l!8CKv;LPvTN$}} z4nHuK55MZlwvL7AdiAwY`*fSZ3p{Dq`EZ_8iT;}h5a(} zwGlI~&0TN6ST>#gGKd*ygiWM-@l$F-#Fq7+rJLd&IF8u9-kz>^ONuN?Qs>PD(5$U2 zhtve?FGXaYc=}J5A;_|hCUAHlzSBTztD*g*;)fAv7tf~(z(>t#ZPX@7h^m?L^ZHLs zjr4DVXMr5LsT{m9$=Ed9xqj~0Or3?^7Ay@& zYYK2lCOv{9R!bAG=U;-vgr7VnJp+W3o0wFqXf`%jlFD=>lh5m&jxq-b&0MM`=8cY| za#=e$PmtlE!9%RgSQqe*fEy<)|>GRH~GE-FS zIx+*si^}ueLbXx(VLO1CUD6Yx-H%PPn8A@+#neVRFomE1R2m{r z4*@U&$`%3H53w5TV>v1&jiQr2Ap9;1V1nlc7y#|;QRYRlpA0uCy<(J>)m>z3rJkJ!Ee=$=vo&FFU9LC^co%wDfNq@|rB#NwMF>a#j907}2 zn&O^RU^+yD5~!&q9p*OTe*|sXwG3o2f0}+l;nm-kW~@$hB}dR8HW!p+j(spvEvlF; z(UvxSv=4*#F)LxaM4|MZQdQ%6Rx01o9Or7;LN@_HY|$nu_6dYbrJDd_*0c(Z@z_Kh zGb;{$v}v(M#?W1qw2HUI{W!vCQ~C;WlW8B~kBA3bkejYkqN^B%o037JmMl;?_S0pp zYE)%{(!Uv@kn099Su!`L7(%9Y)RoPk9Je;Jd;Yz#JMgdPO>og1HmR%uVU$=I=V~i4 z#hM4^TG3GEpA3<4pIrHNF7%^;QED;jn{fxH4gauz!#^;K$DO`i0-(;s^4Y~tWdelIBiH#Uk~Z+PNX*)TC0BqnG74r)OK1a1_m)P>o!~Bg znIVdJ;`2;|8dVY##J-n5gei&zsr05w1tue%$%Ng(rGfn@>d$krVl+?$~>k}Ui9hQhwEJ&@i@@~l^<0<&1`Y!>5j0ChY$Zxm^3+6{8AU^JkJCqs z7Qs!FQZ%3MFf&yOQX#@))c}yfl$B-=luAX`7@K7ZnDsUA;0u~1D|)R_ZR#~BYL?u} zC=lk3i)o&YbOB2XiYg(e^&-}cylul6iw`&&e(h7)=AFKVsnpa6)4;(tde^bDvyw*bGYrHMb zSY;W6AY*!s%u(_)eoZLAXtL(AIIbgOlWClDiV_i=3coaG@?532RHTa4@f1&qw2m2S ziZ0W#pXE0wE`*;VT@w@b=R7X7=!R_}%4Sh#b$|X`rwn66#xhN|a5e~FoTDAp% z`>_3=-dve!V@>&W+G>95`f$9eeLsFMmH3!|O+IUmZ1>16ldjNP0zgx8ew&Xei<-3n za{mkp--UaDt)zO5fO}ksr!>CjS6JgD?1Zlg)(bzx>Cr zNr@kNsn2@I3A>C7GZiR>zk*WAph?TH$a|vI-UF4LKI2_sdQKam6xtP4#3gs|SPX`i zqN)=eJC#m>4GMR%wd#tt=UXFNXn6FYO@ny8N>x6D$fw0ZIIJjCk#Po+4{J_G|IOZWD@)(Nx#<54k z#_(eR!3`M=_#8y_Skp;~IfNA&fi{Scl~-jL^t>kxG8wx?jKJFSkI?YT_z8Q)j|r35 z|LeqB54qr%qh&02%*4gl>)P2&V+mTQ#ci2)S|V^MY1O+R8-0DY<&w}(SDs}@vD(M% zLo3Zy&6b*i`1R1mUDdeQkd|{ouIMb)Hfr1jLMD?P#1UMZDr&Pw65*1sr5Vd0Gtf+F zqfF~hQ`i za4));DPcEg$-L-uM|Isdl&x_?g<~G*2p-PSI#Vc}{yyIOXxNliJ0i(e_a3RDF8JpJ zlg&+4X=0heep}{mjCdFYG+ObTJ;2d8&XW3aucaBQ$%#>9-K2f2B4H0a5i)Bh!oWD| z92*0|EWtzkIUp2&igRT_NLKH!ZC@R3NOC~v$M|HNZHrCTTFSdVvkSMP`bKTc%P@WBEv7S0ia(3WOL3RHHlmn+awxi0k*KVX7jAlxi}!6r0B)j&&CMemrJ@ zSY{`(jLmBf;9z2svF*Z=DSwQq=h9mswlbD;&QqjQ86ibEx#oHF0QlI1WF%xrW9Fr|GYy6**Y{DVWW1i4C!oo z3m3x8G3-c$?WpCbhDNzq8f|g@@PT2I0ZsD{gBJT`$_w}pV;20abr>HU*4@F-Fx}dlX_4_Hu#$9r;U?GP`&0Qyp?}eM~mk64SkV_dVZxpc2$M^dJew z$u_Iy77w!E@998*Q>-GpF`B}I#@Bf*%~%RJm@VR)=+7Ai3k#+OE;f8txCy0t8x&?i z)qg`PwMJmwCzVv0nI?}{GlYOFQdIUlRAh@Np z93wNaVq%$LGoP|gB2ro)^gNiq0a^s!d%L*S0Up|;;EOgyGCt=VQd?5{rvQ(4Dw75! z0ZKv6JspS20d%+k1Sj5k=WfO%WKr$bwKB#v&V~L9|1JEpmQsUey!du0bidSV>2PAn z@(3vaWdgKkRT4S`Vc0_0#z?ZfCzvBbV#($#+E9;8J|TOI;sxjVr#hhj)R9Pgyp2wK zgJaS1FqJkKLk!+RUXY>}aaga=e@~$264NgenleQe z=5OZ2j9#37G+QqfjN>f{o%Nz(79$0XN>3$(m}L$6V*jPS8qD=I_$$q`fd3@GnjeCe zm(IW32F6j+t80QdD(s!Vb7jISOJ3RVS_aS_v&`qna^;%kE8U3cY5<^@-yA+q1w#$^sbtHVP%ELGU zV6B%26EKYRJtk$gu;U};3}TqC!f&Ig;^s%7@iF9{YsR-D3~|JheiNDMp%t&w2*Ctv zuzI%NVzTgjh`9au(LxyIS-6|~M`QScbS^lx*=cR1YommW|1$u1VaIMoIfa7fd2%Cnvd}u*ajdC zzhWfM-+TW@O&qC}aW*L#C!E}f@X%+LA*ip;hD7I+lB%TUseOUz7gL?u7DqnyerI)O zw2Evo75z$te3U>kM4itGq~|k$Nsd9JoZ(=p z1`{D|VKOT1B(RZFHY{WPAC-l`JMUqju{R@Sv41<)z9-tq{gk8{HBj}>+5npg z{?SgEfA?#`?KxOWL0nK(>19iqjOEqmUt@nE{1?414`X38>4DHcLo##<{@p)}LKy;= zhvQzYMNxbcIC#5=muWI!f>&rSzTs2t>eP@Ud^4GgB;OKpZ#X=U|Av2Wb-FPB3q)3P zeT}y_U@Ry7g?KgL>^?0~St#>YeeT>mz_eyZHF$MJ)$-9?bcCIqm|^%&-WKYojR6M( z^?VdtF@ll8-!zSu`VRBfUMNls^bROXp=2EJph=AJkL@s@iFNa-*p^4=zZnqi=lL;~ zWQgz`Eo{i>FPQ*HW7woZ=B?8bRmhEHG6$YnZfGP!L`PZ>1^GnAd}y0}&O}4K|H{!n zKV9!x17uulr&Nt4EeM~Jzy(RGqz)-})6B&diZ;nBf?5pSr`BrGM4Q-qxPzA1z zm2GjZ_Q>*C5lxv^^j`v8oOhhDPG(+G$GSk6t)!Z1#OZRcrA39E;grkZn zi9L?c<_8jQH28e)=bn!ho5P5j`ZuZ6yqg_rHOZPF^GP+==4RvLqi!g}2mUSqY%F?Q|6#XO$M^w;DAD%(xkojjG;qS~P z%+Fk&n}3i{^IXJ#!N0X^Ob5e_@?>@dhz_elEhmhy6AJAfrlqUeq{Us`Sl zf(T^l0nQ|eNyJU)D(7*9{t%jWGCjVzPA}oQ3znL3Ge2kcFU)@moaNDFUrRHV(qhuB zVXnd1rYa)yh(jNacCsDc=PAXHuoNqNc)bY%E6_OFr#dyt@Cw1IEs`ufc}I@o$K`yg zqEQIaepo={aa456q2>j1B{UPjTk?)FsT%WJOpW*oxewxV&K=+B!u^*7c6aHg$D9Y! zS27SXuCf2%J8ij_=lV}X$5){@OrA9npGpi_s;p4`$Vt5Qe8b=UB+Dc|TtX_|*F{?p ztr;s}cgK|s#?SWO2p;EeH+OOVH9y*a;rV2lgG2hAf@bEddDZ;EIfM@}lG{hh5~IqJ zWz0de(_v+^6K1y$E)9gXJJR(vEyd6(XII}-6)HeZTg;Vnzjl;ccuVJDak@zKX=e8# z{8Jyx7E?C7E%jQOu_8&+r%8Q*cn!D;HlVqj=7h?z+Cfk@aLeAO1ux_#G6*W-gxd>p zAF^l+?1>NPzjqclQ5OtKQ<&Ln1-SS*O?t5~?e%Mv*q)HCTNBrP;qL|WfqSA!MuEL3 z-io-}({JpI3VmG9s{Y;nf;RK>9WcrKIXOPH?lJum=C;%|{9*KxWDt#25dW~HB8&9n zN;t5lvK7}{T8($i0OO$#s6)ePP1LD!RQ^4ZX58}W zBsh2z{)@pzNEZHWLbkLS$>fzkEX`QuPPe$7hGp@lfwM?kE(FBKu9!IQdeS?)t7@qr zd_wVhj*~$`IYUF&0223iwz(y38ibtM4{6qD0`ttlqSs=w?v>&(rasYR`2)oWT_bRE zw_8Xc>tV36>8s}sl#CjtK>f1*BN1zT&jN?9j3E-XH-<)IM|~vZ95E9AP3LHc*fy0f zw}QqZGhQ||M1;gP@#{3gBsmLm%3enjVQOX9HfI56A69GO)XOM!Ww=K(@QTS5;`8}n zX0RGpYu?FRFl@*97ycXmG5^B!_(||Pp>uZqo>7ZJeo%rJm~=${G$&xLaU9z9Q|5>Q z5PQl2+*O9U&kBp$jQ|F}S1mBcvz zKLL#e%3{NXmQB0N8fZkqC&80JmI>$-9}ooe*)PQPT1yf!BrlCt#7#i?dG|}FFi(6R;=}Cu=+QH9{U)7i*5M1{d+#n zrR0Tg5r4AHp?=;P%hw4uR3cQ(-*pqoOi&&2WSl28>`dlh2VM+23V5!v@N3E_R3OL- zR)%fi#S$M)R#reUOdwM8Ux2^o@9D;OGX|mzGXKJV&L=AkSXy_SKV#BEuKTC9&`4{P zS|cr1g2VgM)FwVqG-|AZnGS=yenaO5%M>Qa#R9j3WhS+ZKfY296AvABf`HE8+E$L? z&6{3S=0I?V>%FiY!Wp$!3Bl3_O{mi(U*pr#8VgXG#@mE3<&@KIQY}U^A@vvq1h04p z>CJ!!pl6rpnb)qY_1IAg(KGpJ2W*^u{VoWM=VQ6YvYi>1`68x_>oo`&``U7_Co5rE zKMHeuVVnD^jSxHiu6bkKs#g7@VUX-4Bxsxd(Fn~Q&-EW7O#d|?e@~Qj5{6wSt~^O8 zn+PO2VBFcqlEmfg%>wR;oVGETqLrzd%ZL)QE!d6uk*2`xKxWQv2Lg>S%azLAGsU4c zYwX1=o~_50DU>&ALLTA2fd9-tzBUheLEa3SJWm@J9KM~(vkpkCD!Adgfb6E{UGv)7 z+IB*%aa9(Pvpm{Ir!B@_<6=f$*DFx^^bve92TmIP^<$a+1$2;FyW5ve6__OUl<^lsPmBQQ{ zYRfxYa-t0wAHpjpntiG2Khz(>bF_An1HK${6`}Om`FCI^ZlCTUJ;*}}gbY{v;H04C z7#%VvHaG}RK@8fbK8kV2FfeJc57m}SpK%^nMToh@r%QT(3Dd{?%vaK*TmysXw1OC% zljR-4Ed*POdr}dEcHEqb7tel~*YX-Gsv_hLf=qDclkq!C-9(HD&;>umU&)ZGAo z-G5*+CMx#aiYEFEy^M~ z-w!OlD$tRn;;e;DF9j+p$Ee(%{A9_SKD}yHpZO^!GEQeO81<8FB_kz|(9<=RKl~se z^&H`|>Vw@PkQS|u@iD6zrjxN|EcLRGv=M4y!T4SN*_tSMeNNKOK9C5&8u(l8=;v>uo<(#d+dCq^Y4@f5!{8+Vv_MP0l#J{eQe)S< z@W%l-hoV$Iyb(6C#%*C`UP-;iY~(8b1(1tw8}`a5zaMwKd~7-u9v1uLF||6>0=qy=_(E%;AU z8nL|Whou=SdZk2iz}OTPRb#DW<3oQShra+QuaINiTpB4-YH=$@5Y~QJo(}$}zSH7F zksTOGgpU-U9GTe?bfnJbh{DY@e_%2cb9Mjy+t`22GFH&GKNOfBQ}`bR#liLSGOH1zgVuldgWZw$dL5yZJ0+ zp2kdfC91X>T1X{(CYmAbw4fY?Le!>*fGL<-e_0msCPSCNv#mG^8jq=9^HTViOF82I zW$+&b>vQv8#HVR>d5yKSk0tQB?2aAMQnAf5)A>{>3tVMQjOe8Hq&}R_AqIJfM(D%u zF_BJeu0dE6`|-(pj+iujyU5oCc)wM)MJm9ifFlQ?MW#awvDbZX+vj`q0x{KM7vzwxWZ zvP~R6|9xr3GU{L`JIs?FGJV>fT_B{T^PD$5MTd=M$9>8PQ{=kWwT6_hpaQUu6a%7L61p;TUi zDcEZ@XNGP$A))5Eb|~}y(fvomt~NV*2f2Md*0Ro38{^t=`_Ao{uH%Ad z(aDJa_Ijyy{Dd==jfCU`_`2K4m0}o?9--5(24W!%7R|^Y5 zq(qc($YTjqAkGslft(mdGp#QtsE|*2udjipUMvizT2s+r`*(gel0MWG=BvR~i_yyb z?}Xh&q49J2uK`u#X*TeJ8EUMH{l}?W=s%DFwX5Xz;ph{hcUfqP`^S(r6S*BzZ(ziP zs;Lqy)BNkVWPUHq|1C13Dk9ryZde>Xv?>YuG5pz`UHr^BNMj{@rj zv3n8zjOnxZPr|S3!mpkjA1$SPmS!xe*JQjEGCQKwR=sgoK(b|TVv5g`!=jn5?SYzh z)Luv9A~Sk{ZqY;ubtWWGlvBQ2`i@uQw#tO_A=*ZM2+n3nBi#KP+(pnG6ybY=r~2=O z-q-2hE!TYO^Go|TOrGoCfXd>yt2Xh~kO!90?s2GEJT1(>;B&G6n76sR;a~HAQU3+C z$wVw4*rp<3asxVyY%s-3Rku#V`_!-3z zx+VRWgQt5I&*yRv7DNV`$DS=VN%uMBw2%xVBgDTkESU%I6&EWRXf}x4a-9jtO;10Y%ojC;OWQu2OUe34RnWBgm5@s|iFeQC}b;+rOv?pu$xSi5u(SOx{ z3(A@!_uy0Qt5_#vVomGn^{d03TbO-oy1qNyzxP0O1rNM#_3YWMN~39)u>)v<&q#kI zPbU0>j3l~ry+q-EDaKmrAEjrc_8{XRUyE!_Y_@TuzKpnR zMAUUYqmr?^ZA!$%DREoctq8RMl&ul*??Uq-Fq($QIuKIBFp<>HWB1x|qy}H{XU}GRJUz%%5DF z`p;5>;olmrgiAAv71(0`g%+Ef#&Zb+J7Yao23Y8;`FjRt=vq7U?ckKgTclQMSILVS z>C}H~Zqs_6VV(#&7RDJ5mI!;bZr!*!tm}O)j@VKL`=&l)`g0H2Hi?49GQB{5FTuYv z=PlG%R+ZGSl=4}cu{7`l@0T@#lY!k7U4pqBfOb7Apuz3FH%J`SfrVYPgPqi3z?c>Z zI;4g}$iSw0`B2tzW!|-%D~hZRLV4LZAvVZrv&_D2}4>uoe-SGFM zDjwARdpB@ZH*WD&#=^q<7oq<={GXe@@t>ARgUUp+3wcdGH2ohbT6v%y#8j`!qVr5O z-Z?NNqZ;9F!4Ud;5&oSG5@yzclZP(X|GKZGxUop@B;}%O!Iyk6f)%!--hTQgf+mY9(7v5Ud<3~N7S zX(l4a{E7Q;p9!!(lnKUodB&HcdGfvR<>8fP80TL?XdFK~5aJ8>4uL2uwZU4oGfEm| z!C6efMDu!U{^!p>^D?;H)P(z{1|N3kUlmc5_Ih~#o)dSE4%kMM)VcU;(P(987yk0> zi2ow5Mf^AHq`B6#OtVxb?$V5zPTNn-Da%2wF5u-NQ4X;2LQmG7(_oB}Ntm4or~R z1)IK=w*7e8=xCB9v?(R&HCU99th^^mHkA^n{zo${!%1I7oMRs~uaPht$S>*8Q`kKegG( z{NE=BltpFzjMjqL*0mvWM>XEZ?fcMy`v?Eg-!D8LtJd2vA)Q{OP`kFYEX5;BJ!AxN zc&x&Jn$Ii?jipV@YhWiCqyD7?=y;F832Fn>mb7F3VTE^FYX$ty8?bhaJy~sA$_JSf zZ^RrLDYTi2zA9~JwD;YAF>aeF(5@G^7s;;(qB4vFZN&L z2C+7b%x?ryB6YGuS0C*k(tmk6e6+(gnbd||2^k)Cq!U;z@M`T?wUmxeBdpon>ztAW zFUkEw0L`5G!h*1^keFkAx=2SfA_vO+Eu6O&4*vbrL=hwWClDuW5}%g8nYQ^~xpsBf zI^9aP20oR8J9I{Q>6D3cMuzu)h1y0x_J#1zD8~hSh1%8jQUSiD9x^Z_dbH+|gbx?1 z2m=*LgCI*X2Fe6uz8KL?;0!ZXMxgH1in;xmIMx8-M9-7qms)RW*=8LFLSx0ax%A=2 z{1)FVzL;+Ff)z3ViN=^x+c4Wj;!h=hA!*8ZK|lpz;!XvnUW)Y^5I4tUHBZs-OZp!j zr4FA(=uS?2cxW`k+7BxdD5+k;Cy(Zs(1ohC$KIP%R8tz3$e}Y$%29LeM*X=WlaHuH zx4@(!_8vZjnekMfq!DU>6ui(3f&r&OdNi>n;@3^064*EqiJZXlXKSjTnLk=)U8ddS z2Zky_KD=j41#bOfu1k>^hK8n8;KGGGm3zu4?3l3U&#tzvc++Ha)=S{8d5+1$&0LV< zA2(yo)>PF5POxH{9R7}hSqK>^S^Txzim45gmDI{D;fQ7k^tF$#5kNV-phL(msxqRl z_!xSD>Jz#%@E7~phDpS0N|~2;lX5lq&iy7p<2WMsrTsUX zggUHgCMhuWZz3(?Yz-Vs+hZ%KkwtGKKJ=H8+zg?1UN=#&4WkC@=w)kkDk1R)!5W~M zR4NerLjnk+piJgO0V%Mtg%Y#SX?D(j2!5TXh(P0>GJnf*bra9epR_0!(L@tjla{7t zAJ_}#-_$aie$D_%pP{+zFlO>@7BIE&`#ktJ<7Jek<-L=jQC^sTwYQ~BL6xdHl4SED zf5Z!TioD48Fh_)R+sMsQkv}mhEHibxiZY%nLzyU-9qh=&V>FHo&NbI0PK1dCW1Bcz)}b@FIGge2)<}41lW&<;$GOltGZg( zy?FisRhv}Re-WQs?X@&VK(lsZ0mZOAn{?e8BUS?1lursM6J4dj zx_?@dw0EvvaAM}EfD;-l*<8iMCjN!~!8?!EJ}`sCMBE}Z^IwcK)?_??%O>t^>%E@u zJ!CfX1FDu`&YU62h-M;?a;z->Y~?@q+EtjTc29UNk%9{&FH%{=f1|Q3m-g^Iqt|*Z z%~;y5ek@ef@eUdE%g)CN-9=oO7$Wa73ujl?br9BkscWYqf)g_+&6hN_REwDtmR zw4T-@arn2s!&By~Hdjgr)tK_A?S?ebZ-vnEn@h4!y)=0zSn@gj_gL)TMGFNC5wjQi zALq|-YP@y3wTZ2O4ASisY-37+gP5#1_FcIs^Wji7Rh#A1L^hwmShYf9IJTKv^Y>!3 zgJNEY3A}H7EmSB{Cxrb{dr`r7t9~6*A_xy^1}S9>F(bd7)r!_rhLICPoKop zD^0ukOlK4C=n!xf=_IYG=ik-4F2cXQdH&OqRfN`}C-_=?WpZ2UwLV)v6nLmAUZf1Z zn_*g-WwJeKCb)>~wbeQg*xH524N{g;SZR|tm5kGOBtKe+l&5L(_SqH)HG%OUjTZQG zTSdIdQL7(xF%qTo+`=!ZgHhsR7586_8nHQoI4KP}BzbE1N59l;+WG>OC^ z^=A>F&YUR1DC+8wnhDEvfJD7uS<=yf1JT&+b)bQ*1k*w=+~JJIpqZ$=x7hJ|;eQx` zF%xl=!?H0=`fMy;sfWfu0K_nwKBMj%F=P=kb`(i~23=g`t0_s?8oOgB1F)rgqx_+Qs=-&YaD4kYdmk{7dte z1oQkWC2R!COXpu0L@2^ooEz=bjpzFh0~f6asU`6C_jZRp72`S8J}y$4=2{4ztN!4+ zk`9;y`Gn6NKeL(mGWfp&{tN1jr&oVpnz8KVBm>C^>rG}&Lq)$CNlSHa8yR60WLk1! z2I!PQAKfXWTRdXqDSP^C!zSUG+__Wj)k`TOv^+9$@;CPT~4IiTIurJ_xr?|&>1YI2E|eX4TP?BjwSDCButb=lv$`|fb< z+OeXM%GnV46GTlIP!nO8&LWqWG4XD^4gt{`Pi^R|VzS$FFkN88nORMF&%^)o@IU@GPP7Q; z72lR;thJe}KMcU6)|jD;$B2}og!NU1zq3$JFCHjfJ~8OuaQ>8~QOO`XNm0YB}>XEyf1m(P$+aa318wvaAn z4~10pH~o{{Za17~wGh0ywd2zrxzY59Uu-%GT~0WhMBR(Sq`7Eo}aw0b-tw(mioy&m-nhEfF|qIJKShoqqMa`Tku0 zO(CKCntzxA0kNsaz=9>_Vir`PINeJe zhQ(6g$O5WQ8*!jLEifME^o4-4@E#VfLryYI!>xR`(vabVMf^wTj2zNAjUIKLCG3yi zyQVA(k_^r{+(2V=t@?(4;nH!UVmKDbDMWO5h@~^g*U~8@GWnR>z79?xZ_u#uek~P5 zZTf53Z(KC~C+%t)G{*+B6dCO}z=-Ns{IE1*Nvp{9nJJ|qh+0#@{%I!}kk-w-#GPK=f-u_W;sY*YEqh{!OG!bR`HNc&{Uc(H0<6H?Xc(42>3p$m!O~ zEMrUZZRo)1FM@L_GL;5_84Z8SzGxa}rYG}!#f4q!H@jld`bm7woLj4AYm9!Snr%f= z>A&kmSbToo`B-SnV!D{4im}d* zV1&dPYG58CCetLr!YVD$^ru*2Gh9Ug#qAPB3DhQr7r;}w3R|Iw#PA1*`^ zUUQH31M7SAUl^^46wT)Ir>f4Jv!k1KMj4n3)l9xD_FqytrdEj9ULjFRcD`QdKP@fx z?=>{_mCP|rC-Uz)^St0)j5!Vjtp-k1hp+}zp>>6(OZ%m#^7HWD^j~s0k@omvufXI}+&o z%FY!t%$`nqgrV^E$tu~>=`dT{TRLP;!3-6aNffzF-4=L_v-rt0lpL9Y3K=sDKm!(@ z8i)nUS9@DpWBs}Bd{fri+FtB&7;C}gsqv4s6-xED_UMp#HcG-T6QkJeHS+}+^pV@b5~)nMX_^Ck^YBZn#FtvIrErzN3E^ zSf@?}N0cVgnBqV2c~ZQl7PSq6=im>@Y=Q;&VT9l!yNfce%s*3TjqG(SX`jyA8Huvz zUk|wrm|WwUug`|-*Khdv(>r(WINW~c_8on`t(t;2e1L=(%3U6yt&2*rE8*V#Cs;D! zf94Dfi~3UqMZ#~d`C3|I88`E_gKU#-;wgz|0_1XhqwIw%VlWRC;JujC1Meh@rIANYQSpJv_qFNv-kkE_4&G z#l-3!A`JW;Ne^NF-2P2<8Gp@xoRBY;57Po;G!cvIOsqskdt(h_B7$0QFft;SU3pfQ zEq)Bc%paIjBAO@G&Xw`P36OO(sp|l`foLipDdcgX;WPGZw~4`;Jz2qFc|(PhDVt;z zbR&ZXhe@)Hh@x!--b~}*Y~cNJAL0Yk_tbyGKPW8=Mh?zc0 z%X+m=p8=LL!>iv+=kxowWM+DvW0Qt@C0fbpDzsp2ldwMl1=D3s(-K}6y2zEcb}xov4hgS2pVCp& zGu0kE7@jHZi79QYZ)ov&L+MUpVAruk2{rzs0nL3T%ka^eqNX~M^<+Mu>+IRrf4v!F z&DIW>#&`ln+2jt_#(gjnO2eB8;2T2a#=O<7Z}LRK80AA5i8$12PQ-->Z8ACiDa6LB z|K)v=xBXiYe)k@h`3}#)zuSXO zI9E7hqOTN!F&GMw&a;ko{BNjAja&Fqq7p|<)ipg3tWmun`e=*o7Ml5LaGFz3jjZu&>uS$D5 z;8hmMD6@KO?mx_ESajc(r?Fz#vd<4spFUNAna9Hchk=X0p6Cyg5#Q^Iw(Q%C1_Rw* z;oq>#F_hjEjoh8DZ~jRz)-376w!I5_p}z@;z8rv<2Ke6h8^BTJP(z3;>OJS2!uLlO zxF;}$nW}(qfaf(5VkmiJWIj6@Wog8-Q~&*7XKMJ-GU=Nzx!(nk%a7{ifJK)6`TZ}< z-&qAk?Db%?r29A3WsHUc8iw^EnAua9nA$g+FXFG*B&+A5zGyIKOt}W3*NA{Cr;{Y# z&Xy+7F?yJ+o-xDGZnli1ej~(e5N7-%c8k%9xkf)SwCPv$G!NrD2WMepevF18+`f*9 z>T%>ZPYslm)nIM8(0_{7V5ZgrI?SfQ9x2oPk(la8)pEh@F;PwRvwxz46PQm$4##Fo zD2D#hsTtKme|9#XX{~mN*BfapX)3K_HEFdp8=+|pfkVv_vgu&7Rwt;pWK5JQQqh6f ztcL1Y1vTW{x*}8|%=Cwb2|pI1)BnYO%Ks~=+%DI1Pt`-klm*0?9z_5pA>DSS(xfqx zSYhO|{a1Uvu$-?o{~{s}JAj<{rA;6IW-q{%&7bGm>hvQpNG%Gp3qe^4 z-l7koTZqvq+N_AWn5qvN@JJ^fuKvq}UifXCe~qUnGw$LZHQzM<1RF%aS|FY&%4fk> z__PIKNLdoK4Dq`U?KV^87*@=8n}C|13wG{FQ`OO{vMgQ6iM}`{=$W+Hy0YNCAZ;_a z0N?QU@Y1&;u&QM6$Gnb3{~EmNw_D5vt|htpd??qQgH7s|Lcg8E*y{l@Kpg9=dr=dL zV2_Rte%9Ax{b9bbAwVv7ey)s);;NA-jqCai^jAK4Xx8f!KpN>>=3k3OZWy(N8_}31 zzc92KSADeii$BtNQN;88d)rGJTBT*l8e#2*gQiy~fn4NN3&$bm$_q4v!Yh38Il3if z$=0X#Lkh)(IkO+M`~<;&R;H8%kr^$Mw7=Jf|MwpYbN@}FwMC8yJfIC^Whe1mvyUM?YV+cI9-I=f%1hc zfOdCxJumv+xw<3MZ`*sbV5pg3`gCt^*xA22>|EoJ@A&PbLtFT_Y|%JG-!h)c{F-L@ zN}t|HV=2bJ=Y>TOV*4S3=<}(Xk*1-QALGfCt}`*!^{S?8rc;|pRv61ZR@uL0J;vTQ z*5a=$3Y-h)bG0;w7(t}XvGO@+_xba&TH3b;C>fph3+a~NX^R}jSM9%fkNz>gV3I?8 zOP_QP_|%^LqG*rk#!NFa?1js)nwC2(u`%~DyhsTzWP7X0WO_w_WsUHBRh43+2%H+_ zeOa{ME5Y_*Z72HrludbY{vI#$w0PaSlQj2Jz1kk zTk105h%oYzYOz0g_H@|Owh<@eO6smj+xh%rASp(Jvw+_g+3wm&d|jH2U2lMzU!{vCh7I(v}7yH7qgVZ(fpOpS<)V4?pJBoo>r*<1a`^fXnp z=4$7_EysH?TKhKMzli=lw&(gceK(U-|7cA*_Yy+T)!8RqN{z9{trsz$a$sKK-q5HZ-*DH=;!XH8mbT7sDR z_we-}n(Fc6rw&e7Q&&V&(K|C7W}u0}*&12o1}G%rUb#XI_sB2qW5;o##D)78;QxZO zcqMk4Zh9le5mqrsS^W&Efgm{btGl9 z95pKdO!BxmcIDwwTJ9jhF(pVSzZ)sByY}wQSBEu&P z*wM+xe(*nu+`zwa0sb@39P^Z1)44+8**emqkyeU4p%qy(?8DmKeQFbsS&`v>_1YDu zF$LAMnfbc}0VC88+{a!mjzC4jAF9Utku%^)c3yzLo0#yQXwGu4H)1T=#fl25i8|OV zesfF_!`uQRndq5$feknnl7WIZ=rMloqrOFDJFs*3Sb>XER@3dPg#`R(bro^b4(Mn5 z4>hFfX-JRot9jZ?9=O?bY$jCWIr^;sVj(TAak@>|zl6!>^`C5oaATGpkkDuiQ9)I(%eo$$JoKR^( zDl#Z7lPtc8rbZTPO>dZ%<*Fm&-K7 z`1A9T$!uCY@B(lz4^R#Fw$hkx-o9lY%jHSD=2&8}{3Fa=it8A@9;YlJF)3#$H~z=@ zS_YLMYS+?tz257M7)z7c&5l+Ps|%F`*kkT%0mSU4N#%U3NU{=IW0cLeMu64t*#26> z5@Q|9Nsq-_g)`Hju^j2daDI`Y-yd)L~XDz^iYN58rN8r?^9Qe|(4zuSsbL<4GasH+2dm08znY;QluKSMgM|;6W3S4lU zt(e2HN~B9`juN~u!J*cr0zaLDB@J{{+wc_*AGT!bT~j9ed>$%7R`loZ-{-Pap2mh!TcZN$nqqGB?)OnR1@e87BZVl2Sd>T%3L z&nU>Y6s+$?RynPP0H*7B^iOj!{=F{BtLN~0JV4WsHyEY!zs zJR7#fShiy7e)e9iZ*C?)(OgjYrx&y#+e{4q1;NlVAoRC5f5U2Qs+)Y(_cvlJ4OHh< zs&~{cl4jE0p$d;0)!wE?IoOy#NNS6)068>NGpWlc2b(%D0WdE%;lr4U)Yf zMhJ9*ZHUybV&+PE81H<({}KlyBBHzUy10(^n*G=O7g7aZIR6qu3oy!pZD55aU?VV2 zyz=9opZ@~<%Nv^JkH9jla#K^!%jO>(ac|9k5&jkcFPQ(>f3#_YZT0lr{P~@w2QA1Z z%cY)e_%qBCPB)Pd;U~;_(vuLoUi8&9N=$RjrOOwkrS)z6N$(~I^g=6b2EBy-mgcG^ zv5~S~^MeY{@8*=mP`#yroyH)&C8!0cCH!*{Hs`0|V8*QQv$n-E`JnPlj%Ff+C2q=m zj3$iPseg>%?sNQ7V2qUVg0;s7XGqeI2xx}RZ_S*U1LMLYEa{m+m9Tid{!M}i2~Ai9 zPwiji^^*RZ2%cXl-?A-g+aArR-?cgsuooM}fj!g?t8Es~}YX zBxgDm;>?tMj@0qA^n1+sFb9E!Qw~um{8`eNk19XDm@ro+#Fx!bAZ?}~a6uM_+U*ES z0Xjm9`-*-FVa_kac`4#=7bAXRghHP9`TcwAh8K0IN*lsxYHe?BLUPZh1Qo=oVGxoI zaiMj}Jhk>(v|B>xxrf_DS_XRH+wgbhf-^AMi{iet)9X;dN>Oy=mvQr%`TN?7;6Dzx z=z=w-l33#t89(M)o4n}*GK$Z{yve3$A55^PRV?^hNVl6(b7Y*jHiBiHrSn=m(Ko*lU#`wp zj}!@%>6?P|qY3TVM-sGz$Lw;lXAxA)#W0|ykb|s|d1ez&I^VOsN)uHZQ8b~hj^K3^ zVd}pF7EwRFHsz}&KXDtgcF9Ap%rYvn3=5#`mcy zTROx2s9^)M){u;}c#mz`@k{E=B;mGk$X!zW)>>XdbPgYOyY6XovgU8KZ#F4_nVQQC zT0oL<(k~Nf6H%BiW6L5k$iOxLsj2oFGt5&kf~KaP@81Hkw$pGU#J^@B2ri1An0QS? zVE_|~nV&FyxQAc7Jzgt5>w0d+nSZ_?VKk)~g%%-*Du8JQ#JnReO`4^pcQ}eOZ;fTLF`?0Fo`jXsu;m zZ^T&Z+M2JEh>t-s4j)XOBGCy9L{nssjMpbn(WY6D0u@AJgqRedz#(;vLP!@dNaE$i z{7AyhZqZ!pCo8;F1lS-AQJ7#%e$jeHBX}W~j0lr?BmJ9@ymnQlVlBpTa0B~%*v5`# zn51u{YmT}J`qN78=>B6OCyL1fComa4oBtBA;;D==%*s{?1PjkB96)NgY-)QwjOlO+ z3-c-p!eIdJmH2QyZy4438MN57`sf!wll($Q;2ch^(fh{IBo=Ud&hN27!CS15`WpH3 znfdo41%CueL%N8&q=|22xyJPlDPacV>(6Dy!Jao$MlfzDa!7#~G!drV6Af##X>#&SLjJ83 z`$#M;gAMkAQTu@&LlH$rqhCrwSx$LS>3<9iFelF|4B-2kP<~$jmM9b3B@q=0jl=BY zB%;7w_RV_AYciqYj41!fd@L&B5|C)AobD&{ z4vAu{nE310dIKnBfY{cq+lqi~8Zv1#3AO0P#L8yh=!OtKO@>4aCKE$_FX?OM^Nm5D zDY*{{fQTy;A3!sQBIv%orqWuj$oSRzH}|yN*pwUYv_wTJm&6vB(YOU?4{+*#asI`6whjtv&*4AMc^)vgVYoQ| zWb6~TFtHbz#|@awc``=UwF`LX%61YGbRUVfBmOPi+qFoUfu;uUXaLTZQ7gk$)p{oa ziPnoiHm5qs(J#;D;)2-t+pBzdBgR^tsceaBy+AMwd?5l9%opJVlvz~8#?}2YNl?eh zE=F@{dWMStZ3`uJ+i8SjU{j(hU?-Q31YJzUFk=(iw1p(nyl`E_YnKC4Lw!)XS}P}p zkst5%Eh9+dWJ)%1i>Qbu7NXnKvtRJa{f8@!T>jKPW?p2d2iDwdt&~u334*d7$@K3@ zS>xcN#m-EdHe$j7mbGR!+AYCT{afPODrhs0@wmg1h|Q?!jV3)j{%opS>{uA|+GAvX z!@r37Irw}226+1SUcC&)^gy<`z0NV~CG)a*YvDmE)3)|s>7WcVe#YCi8ZEm#j(TR4pjs1@%bNoI(3t}mOM_3#@0;hHhzz@DM?QxH3ro$}Z z)R^ZPh|d}6{ul7I*uTwUweKLCCkA-JScbpwodXNcSAO(|zqj)BU;Wc{*Q>Hsg0@S) zltywaNoON9q-k?Tr|V(ojmr*GBgOyfI#OZe25vIhCJ`|Us8)U7+yH$%YETt-NP@ZuD7cA)NJ}RS@ z>oO5j`?asrKLk*O(`k|+k_mrJRwK`vPyu4zJ^#{Tmh1+7%j+nW$h52qPJgtRqL%#M zvd>Js(0^dEU`{>P|A@1uv(L;wPqz+lF3C71A|2-sW5HCKn2&+2uzZKmM{B0$KlZ;Q z{2PIvg@22*3-dq!`mg@xQk?Zhrr^3vLF!&iId5T>iw-5%LKrrM3~8cTQNwnzEK5wnx-5L+HJ!1RnXgBH(g*pCLB9G;ei#gLU2C%+8f5^ zLiC1@-n8(nT*rx6O+2sAe>>}4KFkm>VZ@~Ef}C9OGDR^i zV;8s)7Rvk+E43wvPv&4npnF2~VZM|WWL)mw5NQaP-|RXf{cNGLm^kn+%obyok!|=l zPI~)F=09PQPhf;|(OmAW`p+HZ(Yl^zJkk~x#;&!Gk>h(`g#ShOcYe;awi!X==MXqV zDA8bv2eh~y>F!lOyphKG_4nUB-_T~qlveL4GxJ2Ay2wfFd=}(b0A}(Db+YzFd0`?x z$$R6zDp*Mf*_#4&BA<>(bw-<`f?+tLvJgF?Ifxi#rm+`7Y!emphP<=&xe7RHU4(jb z+xpwwTK68GYuej$4vFJp;%Dx8+ za=o}62|TIeMFw_rTo>k>sM>DTLBuUSo83>oF0?!$3Cbn-FCTuWkH*q2?L% z7M7V!9QwM@TSQd%#Dsx>IX#nfuPB-W7y8Ze+#t}n5d2=JDlJ^XHw4dpz&i=CnP0)q z!$|-BSif7tJB|V2_WXI!;SLdhvSCJ!0avwn9*vx_xJ~0Y^L3D*=>c=FPlO>5R+@3u#iq$~o{Wx>3IT)o@4+igKfz)bz09G|ZI;P-!T<(t3u`oW7gW-LY6 zp5b&Vnb@o|0?wHdNY^$K_*{}m5MnoNBx(tqQGP&lr0mo|)Lw(2zF>tOi`;bd<78sE zjUvoKTv9B`JZOZ1C|h~hdD2|?d%EUz6o=cPPDHcbIJ(E8vSdX@fd zj=6f~=#^$Ri_&!*e|ahasKy;rtoNqbG}LdM`_4hx@Ovw#8uJ zI@P4ea{y*!LjV4G^KYS6$no=jz+4eOS8En_M)G-PfLKY5X=rb}xpE8{&|(5yQ{d;m zzcFK}P{-4$LXa_Sf;0~~nie9WRMiPNVNAjrIUbZFRcHuOOeh0Z%+Nu*oLmSYQ1reC z(h_f^$q}R@Ye8|S@>&>V4A~eNGb)Ru9$+y`F_rI$2F<(v!xtPQRWTYu>I;lqDE<8Y zCj#fugr(gHU&jCH6(pMYm>1Q zNa#M)mXMi+2>F>gzNwdLa5GYKpMLf3QygPem_aH%iW^oXM9_#wWVMf!s2oClFL`O+ z_j(*KF7C6dq9vIpPoJ!*fQ@zkD{<4pO>fLt@>3p|eSjB)?_8TK=sKGiCeq&#r|v1! z7y!jrioNSU?0ifwOOFs));Pd0YB_s|tPu;LCNZ0XnN3(YOw%j;5l1mo!w>XP38zgM zFrSGw%oY>1ZbxdDt=~@n1pV7FheB=bbRV+Fzr|fZW3*0p38(V0xiRa?fq$ZO z8B5I9Fr2R}xc0`s=R-TORz3^=K(BXS8+0ZE{5;J2)8WzMCvvM*bkot}THC|J?qglu zY*q1%EAKJa5RFHVxqLUz#|3yIyuHkTAgfGxhxnD^L<@00e8l{)rfvRfTRR7T`nP}Q zkH2<1!yT+T5zqoF} z^eFb9>ztM4GSf`jP+);IF6gAcB6<9OA=;)>2LvFlB94qO1kwyu#?R#RqD?JfL-3wU z5tw}s;qT|2k2UsRlG=6kN(_u*-cq;`%Y54hvC4^tFQK)uF&lQaXTzhtXBGlCx7UW- zS9O|(z(u_y^M*%TZ0z|%XVHd{%9wwgy23|3ceE{5kD0>YDf8P*5@wm^A6Sm?H-i@Z z3nO8|&w;;T5@w?d7P|Kj_J-A?U28B40&u5ulr2y#kag1Z>+J_<{M9KDuA@evr&Si z)_NgBlcm_=C!gLQHr|u8Jv|tH`pL(tYAQeyodRjaL@9(H(ggiO{if_WvW7`toZoC! z?|=Y=${#W*sYksn#WSm{-XtT}*t{tct&t%07*4U)h$Yaup2?}H`5V^B(6?ZEA^f8O zjbp~7zP5G5={tAd8`chCVY2= zreY{VmCO}>@ytYG6RM>^kyrt^4E*7t_Rsu_ z?|yIi=-z#8JUbmex%Y6`JCr%2vymXnpe|;M37<9rg#HwCw>M#b#DdOci2}Da;Y>7K zPHUqk^YBKp_b*0J+3G>Cv2T-Plx77+i;G2#lTPdZ?g=E1D44nW*uG=oaA~9}&TQ5GBbz*Ia28<8|9}$b`1HWex{^R_M$iX#= zlT@=SJDa+fvyuy(>aP#+cH4zF>I*7rXARXECm}6saRwPNc}jLVYncVfmEn_z zyTeEKpT%J$waB#}l97?9o6#4-a_*rG95I$d8)f26#1BI0$%p{%C9qmSF^DltThI4V z2|g*Z`veK@HbgzXqcQmn#ta9v5Hxf3EK8(Zz=CPB`v)fc%J?i985e>gbIqDecU$u} zMCm_FMGG_A!G~IV+H`ud#(Fk%(DhxUOCR?tzl;AQ_50>xD4Iw z?P61*#Me8j#9jSOfg6y2+$^_^O?q+S6wJ~bO!t4N+)5=+nNwro)1vA$&&cnPl}gv! zAz7^Pt|Mp)@>ha%j?{(QvM(U~u$D~1A_rnd(}v9+5C@d*emw~6Xl6U-GRbeY{I+tb zGOzXQEQLh)=rv2x_wdw&4RhzX2l|90SrbZHzb_6nGT)eW&U+KN-o%fW6Mgn%{GBwU zeUSCEpd`&fXVGMC(G$7Cse zEnh-zHl8>ZZa#=>pXWW=7mW=%#Jy9V`9WqlIEwpix~%|sM?c_x`<7FuAV8Fx} z{H5KWB&AT?uou3cCQJ9iBrF0LT4ZkO=Y2q~Audn4L*Eos9zjuAF&yikKia3Xbay=E zMM=<9#q3FCCV_870<*f$84unxc)m@;{hiQV`6GgAg`7beL1WRxR!d4;%OP)HrJZV8PBt4>F_vwhU415c@|i%lsAmnr$1Vz*MQy;;3=Ev_CGoWR|*1~S2ARoVQ zc{0fU;vPSU*>%4DOfMaN^pO}$`tj)BOwO)p&(+0MCPA_-SC#gWAqpl&X|o!i8J z`oLMzZ1M1W7qf0(zg6-kLb8?!LBCTAL;b-biRio-n$%0BURw9Jh!n_5jy!YxfzN9J zL+s&Vn%ALroOu}&~PUP^Yk2-rCLU}e%=ji=WW{vO>Gc#@l|me!z8K%%=qXJCEYDb-x~gFI!UV=RhWzq?_r7V5c*q)3Pe@V4fy zsm@iR&4Rn!v^0On(ubN~3zT;++5q1?enoO~0DwS0e&<|IG*{$o>9fsCr#~wpXA4JE z;`rIi)L_0kdJ5}YPc=!#rnf<-#IQ@*3h|pO_le(v)hQzZlntU*xf~+IusvADql?Cf zYUlvgF$VDV&-mpmVwoOa_UYCzCzc{Ln_9$_+Km1)R4Zd=2f9tKkw8ZPJd*4!CFMVR ztUb*kPhf@#XZyZ>AI^@==9v&U|66y6Qqr`}GV)ZN%gtJS%GX7E>2R~rB(Tmo&{N^P zY}bD`daPz;zI^ce;rIMRxDW?@Xh&0_={4J@bT&V0q;X*q5yEH0p1P&-3pMO=OIs(o8LABr*u*k7$Zk{gcW$ z`M@y9s4pUp;%LWDd7FjpApW;-@vL$3QDVaL1Mo6tm;K>_%Z`bOmHK$vMqT@RzOk|8 zx~v3c#O*F<(DZzhx~>=5^Yvm;g~;v40vGD2g~MNkiyEGsAxRUma?k?~`0@0DFGq;X zUFS~`@gl@7Qd&6&AfXZ!0z$TY@0$@mBaUE_bu~@S=}SP5B(4f_5b<(N`B#$n8WSw= zt#mm}o9jI?x_=o#mftMdw)6B?H~Cj~h;LbR?u3u*#`s51=xZv6bO+MVWEuaIu5}L! zKKXXpBP_P}CMzWT+DmMiZFwUzsAoC+E6LdNYp>qVMNlc-+2EVoPyV-r$SBd#3f?s& zf{dZu?sCM9tI87+3FJ%f!zvIIz}}b8pDowp*p^UF>h5YjRcPU_Iz}qn(~Q*r&O0OP z6rx^@-4bDp{N;?d-q)h{WHtoX(`0i9n=<(+O~mFKpCat=57iAvWxlOxH%h3FcPBi^ zENHGwNo$%nUd@+z`seGU;m!7t@KxZWehnG_fnr{sUi*|ZxI#=gR!UcErsb}K(!)9U z@o#@aMHY=A-lczVDXPGzz_D%fHafoH*6$F*7eXBOrCVj+$%29V@k0Q|jdepP z@05(;Sjp-ZQ1}AzRP?J6r68f0(UDCTc_Qf%aI|RB$c~>9!UROU|6ba-nC){u{WwlN zCYTR&(k}oV@$2*S;hIo{jlazH74ru>;L6@Scz6TF#KfC#37{k#OOH6t#go6Q#Ib6= zeEAujKb7XXPSO0{haoORLhF6vKpoFd9BYb~Nu>y9F(b7wZ&N^!qxLF0u^|BU+(%)* z1>#Gzbl;f=nJJPSw9gNj9kLq5^=hCD<2G!zxrCnuo#*+QHzmnA0nAO%#1oa&#%e&w z0N$y89b9^1BG(1<+j+m5olng0eyhG0A{+4F%pp#2_<|d_y2rJ5mDz}cfjWljz?a4I zD&&xsfsk5P9cG26PVq|Q*P`sgw35Zp{=NOVKeD9KPLD)h(3M(et4lpowEjQ;i=Ap? zf(_3HSQ3BH8{zxnP@hYAA$@&h!pSE5Gs-NC2s7zG9537&ou^nH|IRE5jpeE!0s8z} zd9W4ius#;cbhUIDdm^xR+NT2!5zY@eId)M(n~$&pI-4!RG=DQ}wNSh%V6h)LQRQ>k z@4L|rRRcyF)nJL|;373dxrYReI^>hZIrW)OjuCrWK05JVT*U*$h>m_H^mg|s(urPkulj=6Z0VIPtW8NJI51VsE zU5ZifW3~s{PJL{)L6^vFbk&KC)O3%9GEpP}6%hDM2a3(cuFu3Q?3q;{XUIfzgg_V) zi=K)YF>c8*3leY%-*TFq^6 zkq7*JJ6m?l?+G8SnZi}fa_G6&BN^X;LvC`lgu!F0ao>=|9KpT~^?r%N@G?pByx=b6 z#HCU42jwXZjm5e@K`-xpeAq?gpOlxkSK)Z-n^fxxWQo?e;{3X#4f4p48CZm^UmN#_yum3nO(NC;vQU;5U?)1y#_|RlJf$hORPQmE=4x0V7kSveBv_`& zQ#WAsNZCW?#93VRL)f&IX4I1_OR@N!Jke*YLel}T@*!Kh(={7*JejmQ((%MxZ*@K> zmXqi4uvJmUt2cNO)O&Y)wQ{@Dn-9QAt?_(AAAarecnrKONjTPW=Q8!QDAK@_B4dQ@ zrpfqXKW*4_@Yf{Gk6F0PDO!23bK{H11{P)iGKsy_DZ4Axx7SSEQcE=(9hzUuFgog8toulZ4HAzfkY`Po(kBHMH7Qf}mXYb9E z2;MIeTnGN`;fhMZ@bw0HPuyT;*yo_@AN`4xw&krCR=wiCnMt^-e(4-=-^?qdF#dNt z+kV-7IZ1;$s*ujGl_?T(e7?1to2q#Br9Z&c9o%f&kT#4@U@O!pJk(J|bHp(G8kI~^ zGKq$hQ9ctpCAJkW#4)-}Xr#-nP+&PPGibX5vfIg7k62W=+WORd`)_C4%zi#cXCK<^ z`S@XZvL->ie^MroEy$Xv$Of8ofq${9HPI|a^d0_}UG{Acbf5mUu97m&h7%rejInU_ zi&_i{j}z1Tk-!#isUth?6m&A~lpEcAX%fYwd~`iFaIC)IN3JTnN+t>$`sHM8*ds6P zb=uPe4-E3RPZRpUQ_8I+qFUyby^SS=3le*N$!4_YFU1w_>iRlx{V0j>} z)XQpz-JXyT!6g@1%=Z&pQ+~Q_5yo)i{KRo-&il!Lal^aV%Cq9Y8`%}Jn>4Uw6MjL< zQ@Tx3N4odoBybq3+VpdKI84NB-4;h zxp(=HEHw4?%Mvv^;QC)q^`JMba)+|#X3{3h$X?r=ZDeGo;f8DHTI0DFw*3H7ec21S z%U7>nZzw8Mam;@NJa|EdyarQ1^du*If+&gSeFJHXb$WO}w zYIi;5e`yrdWM%|w#~UzIA-^)lzPnoaRX_P5n>RX+W#c8?5W`#80$O~UJmem_@ivHw zuDT)wFX9iu@EoqQeL0|Yqf>^$(3%!;8q^~g2A}C#N0cby@_C1D$n0OwY=33AL>H}C z5V`_ghGR#~uB;e^9>i;_(+_`-sI%%2@JQ&Omk#@xU2grV2KHgAwJQ$lfdhCnQKj_IzIJ{Gw*yd0NGpO&HbY*f{H$S#=1H<@} zg;$(ZH8a$qO2nh0TzV0#a8N>+OwNVcNU&HECed2}4XlP``&#e$0ViI-Z^r0xY2TNH zUuqNG3G_`+ylP7cmn8NCVMtu^`U2Gmry_M|$gS?DojAy7ssM7+#!AWG)C29N$1j!% zIaLnKwB9kkD!K@RLk6L3-UNb~0cSx@rV4JE4WHj2C3*y;D^NJ#MnZ;X-}CD@%6gi@ zmxpHcs1XNPcK@aw-eem4ay#wl4_&lsDG!{GOWhL)@(L~2N?xdC(RN1Q2!T$Vc}|7< z2NW^h_hC%6ln-rh*udAZlqu`s_i=pSqY+q?HYWe4Aor2E1RVhCvyx|z`vmRA(gf`E zR&OfXj*;E`3w2<2A*;=FED;kvf2`zSGa+_0iO$2-m5_2aA|2ZRlBfUHtUyOTE>AHl z*AFijjgg5t9qBpU!Z`VZFxUZ9*{k+z#4MU6O_%$ELF;ZO{tBq`s!%2_s zZM&%MHO|*aJ?Fp`rI6{lLTv5AyDr|nGCek;CBd|L8<>srE46;Q!>Ca$9ror~0Emb7 zKpjnwkqaUQXOOSuntW6}D-9tWfeJv?igb&H`hfpYmqvq$*h*U`J;qH{)9IVOuM6Wc zrwdLyboq_=@@f#xF%-Czqe4c6UuDYSOyk3Hwyq9Z#H;8Y6)4?%#6t%%gjl4>D@iK2 znzT)SBN#y|d({S+45Yj>|43zs84lg`56~EN_$GfdF0p+QU$@6x^Cm-Ekvz9k`Q0**nL&j!b}=gU+~%8#yPd^+ z0+Vw!Y2+1?JEjw+oXebMKOody(Nc+2U|Y74@Vd0T1?5Ecw(OZufN>@KcUqTPL<7J&cq`mBX|SXG{l$Y9cH%6byEvf!1Ky36^iE)^*afs!nd$vve$ zL%gpnX$l)`Qw*%98Bd}1rSGI9N=;$n6Mg|N{Pzo&7)s5lck|Yo`a}nnltNrOhO*I!*!ui^5(uG9^5*0B+IRSf;Tg4F zFxB;%_9IF^1pDo@GESQ#Ir|6+<%G zB*-W1i$Z=8%faAG3_*j=^TglTfd!GiPN8Bpb(=xz`X$gOJ-;gpTab$J2a|qxx2th{4?;pKTh!aa#@UN7Oz^rni-s7HUFNtadV+p2?s}+!Tx`m-FZe;E1+~;Njd`SVk*%-u< z4dsLVt$2vHcJyI>>7Hi`hg$CF-4DO!U(GnGfH;-W$SwgWCW{Jy0^=oDN|2}It(dd$(;)%?iaYxmR6o$s_*S?+Et*ex3^iE&_cnQBC5Ut-6&FCS; zLmjcTo~+=XX*-q4i(1OKHnx619l5qJHMZ9%8YsY0)VF-B3YS5-RUjOy@K`rUBzHP3 z9JRF2+h)9NUxMHPYHHU;?6uu>Vn&UB5Z|dsb$RZw{2V>YD;_Jii&Ktyc z=9xAXy$w{>JRgG(V|)_h4~GZAYc{Bm020U76dP!0fG-Tg!V zBhd!)>CILYKO~ln9<9f=bY!igxLQ+w!{j+DMVwt?55c-(;3tvT|J#{YMawA z+@{2_P?F@k&;(b;D5>mK$tFEDo#Y>EHn;L2N_tIVr@7V01rMINZYCWP3x`Ae0J6-E za+qX?H4&kjpH0OpCc9+o-fXC1?Y&1Rnof(0R-f8u;ztHN5_)?8A`F5kE(Ydn08095 zsYWg4a}{b(b=xlJr~3qu1vnI_DlT|46g{C2GYt=#Hp}2Ja~ZG?KXlZz&GXu=U+G&7 z;d`Njl=O5fWd~X@J9?VXO)F=NlOikQEW+|qS9~7w#Y=_5eOL7Q+Y`KbGl+XNmWJP& z@zE+R+TeW7LO3}09ct7v0JiTXuN8Wayl&~Q1BZ1-I z9~=>31LmFNgsTYh3}(XHGn+*Hic$0V$ zA;7&DNDSuCA)tMHAjK}d4Hvf67a;SUGCa-i<155ftpN=ihND5DEW)G*&+ET)^Ww=W zh5@xMfmhu#KfaAF$bcyZ%uwXs9L%B?r~9Lhk<&Y7WtAJ5la|v)e6n1}FhKPwn_zjW ziA(QM)-zHKh%@4)_Tznb%5sp$#5hso=(wRa*V1koXmBI%hS!#DRR@KSt5ks2lf}oS z%83$fI`JM zz@sBia@;dlcaG(ad+}EXCZz0RXMP#UVkLnNCS!aWRp9YF66{rl7|Q54BRff-yHyE# zxn9?l^&h^sN#Iahk+j^9m5}ZzW{C4Jt+rz(O>m>;4}rX+>|xJ7H|S5LXO$tl!T%ZS zaw;0FOVEZr?W3752%_J(2(9&ddBE{ih=W+opLt0^4e>BQiv4|U^M!^1>-y|G?`AHXA76f9<@@|Td?AZhZJR6 zg+vB&zFO|)5R{VxSPsglSA2oZ+^6|1KL*H85q{m^k{m~*9b(Anhn2ffTFaWJ_jSew z;x!A<pM#I+0TY(gc zn>a70K9?y%`p7ck5ThPrmPY7!Bb{d>1YY?0XI5qK^y!76*lk!54L872b-1FHS?smA zk(QiO%MSHP(mFJI&)>gZrvuz9oHi|0_Lbj20ddV8LF zdwI}t%EMgTLyzT@r3nzXHc#$&`sv@EDej&JUXIfJAk3NUEvh^E@>!8VZ?GhJeoTqA zl05bfdwj20icnwoFi?_=`;!2W&MDh6%?PXQWmkN!UfH8-zBtqHSFKQ(!u*d*)UCI~ zTy3>IlJA2YVN6}JO2X!e$rUt{Dt%RcS}CYPWGh(i3serZqQ!s6B7!_Z8hA^u-kkYQ z^;-Rbg|&s?@&4c1)q17DKyY1_;}(ekYOOh5TNX*dQBixTuTvqgFkt>{oN1JW37Ki# zvxX8<&I1QkKtN1=KvSGrq+0;eLvbBqTbDFq@wbBMS#gse3V%&@|F2ZQ;+BCI{ ze@vDLD%OG-fsr6jZYC?alP+E%Cp?gK4OQ?=cucE1;0 zwWD#4t&WVfq$KpXY75by12F}%s63i!-MF;=y3%_~8f0vK;^G$yb@203=P+>$RJMxswpA-Xsh=0tBQIcpL$o#VRMk9X9ZzJ!p8sBrw!M_($irDg zAu^}e?fFick8Hp3=pbX*wL6)3Itp6Zfu!`_mA79ISgJx()A=ya+o3KIGIR zz=NWtUsu8&w+B_ZBmho|tP>*sD$C&At6h;tvPG$mUoCgHGhG+@rO4Xga6obZWc`kS zWjl5yt3|xaipN)ZA;;hC2Q!ad(82~zlG(6%E2XDw{A2;41|_;87$MUbl%oBg(tQrem)>0g?5QR| zjyEp(KVJ2|$Pa~9N{?Uu-~>nbv`emZm`fyH2CJ^LnR7a{C2E|BV|wD#Pzb_5$=KN{Ij;o8tWDzs#3ID@y`wS4zGv*~#L zjCeNUg1qLxKcz=13g#5f|M_7!{QYMCEfB$rRM7gI5(GrE1@!P(qg}kN2zz?OW4Mgt z2N%6vk*$BksVrM`BD6Ak9kBLRJ! zNh#cca`oO&AY*`VUSu5dPkqEqHS}{rcWmEhs{cKif4thX4Y{}stp-}IyF%tLv=f@` zu34?7BdC7AEmAJ~t$H>qD=ls&DY;`GE-&F~(`EQ@06F#a_Ll8#Y`N+;?LlZFZ{PO9 zmVq#Y={9uN26_X$AOB31{@K1OwvP$DM{nf$eq{%lhB$mc{C%tYRR)%O-|;v2`!dNg z@n%tNt?V-N`}El~$W+8TylN5q!nJmH-dH)N&cj2dyR*Byx4Wg~K@>?PEp%kI@GwN)y{k;m`litzcSg7!oBbYP~AH!%h_CPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf6Z%O+K~#8N?Oh9a zl+~GjXEKxfm4pe`L_h@OCU@c;VWAXJX%Th1)aPk+@%F6N)pga@wN_hMT3w&6+tR1H zWxLDMs?}0!t7{RdmvD1Yw8&LJE(r-C_hgcpWcGc}Ka(a9lDYg76Uh7UasL0G$(i$i z-*>*tIp;sW1APF+$Ni~rAcu|3Neqt%B#>>S|0S)T?Q!kZ#V-Py556mByA$S z5I|sEk;h$PuJ$l`TmaPeX)IQdzCw~s?+vIRy-ZroUh#ps(nIJm0g&;Ru(+ReHOV;7 zUI;n!-;-8R5bv2QaYoz)K*mpC@h_yM=0e}WOQi2o5T975@j=`KP<-5%$?)(Y9^Oq# zHW&LIyrgFs?tZ^0->neWcEm*h)cOT1o*^lOJR6`;_HOouH>^|pV5AHUR`<`8A{>bK&eoF~ioI8MfMzL)an!GG7VQFG(JA;p|5l zX*o@j61te+43<%Y%>M?9*GT6I^99}OYr%Vu2{8$P4Y-fRbENLZXU<0Go}UZyAZkP{ z0RFH-%vILeIgM38UPOhcd_Yj@{mk!(`{@&Y7(5Oe5etCKm)(=u>Bs5#CWY{P@Hi|) zBmgpB(T*JMxoGplf#xC>MG_r_EdVlKasIocb1B-11`1Ixr4Yi(6T))B%5YgI$?9i* z_YjZPY_fRPEy5B&aCqTt&v5NT5p9cj)-Adqmt65=~K;19)8uAULGF zg@YOu05V@QSzAdlO{PfWtdd0LwD?=em`Z%S;~?JId7y`YNG_ZdUO>#^SEajF^;S=6_WAMop)zdQ zvL73^9KrTIwLm%RR(+ZTmE7I@gA^WklCWe_E-slh0^`RQAU8MDv^5ZDZR;&I^$tzy z%%BiDomsQE(+Mbm7g@wKjVNvOdYj?*w-_p=HojlH7pvd?9D#~vi-T?_rk|gIiRb4a z-;;qvYGjMA1trI8@ct*|*tMHXOkgu}6LHsq5xBT$GKIB zoM`m5qP(sdN6KivBq;vvMZ?o@>rGQJ?!p3NzN82OEk1i-3+n2;IB=j8uf6^ktlP}6 zJ&o;=&=Q>(Hz);}$u8t%xUuZIDVQ*^(D)%6pxofJiT*PQ;I}NUway}kZn(U>3XAS} z1!$%QsoRrrNo0^%(OB!p_f|~B+o!zABk!jht?-(9=>MiU!*@Ptan5Gtf zH}rko0eeCU>u&V6VD6Y~eCw{OjE!%l5Kpbwm_q)ix*Hmr@SB%6;h7irAT`rvEMT*~ zFQS6Xz90)vK6))uQWEVjN9!nn>w?E^uvDWR$b7Ba(bAEqp_`Ohkj6Tbok$_m#2~|b zYT7Le&%?aGxd<+5yiC)XG)#ig=c7jt!2^GPvE?K9FIk!{pHa)-XbMn)$$Zw4lkUbH zw_Jung9gCQ-|wtm@ln@RuNb>z$t*0Knums_R?~shPPLZ@sP+2U6Xif`!PPRWK*rNc3-z+;#j4rJ!iX`<%O8EyWaoehoDe6<&K zEm$(|e59qP*yW7HQ}MO~ItW0^1zBjtT1-%b-83!B|L!vU`p1j0=E()Pan>Mq`4+gd z+{hV_5jOJ$)t8v)!l;656EWR1doUI+o`H$9&r6ri#wDXNO>Zb1o`VE&F#?FU9uMnv zIZ#g_+gFN8(T4RNGdWjQ`p{C({6it z--czu(x6QM>>3(w)+%M&Awgtjrs9f%40EH04wVN#2@i@|luztESjCXhX@nW#je`PE zW~)eR|G^5{e}6=>6{Gik#`aKv7mmn5GJBNGT7N2L2YGGKz6WT1eq5G_>xN`1ri{-w z=bN@2hPScVFg6@0KR9ya1m4|M4Gc+Ql#^qI4-8lo6yyQfF8txca#U2*m=H4@=zG=G z_1Lhj#PqBS#||>4EzS^+#cPB1E!AvS+#pSzJh{LO1N>pz3GCW+m>FmHjFONP(!RCf zQz$aIX==X7RyAZGC{!IfEEh%Nvru)k5u1wl=&#IY-9hH-gKhh;>i|t+ei9?Cq0*M% zr*TCauMG#R$AZk0DRF~r_{forUW(WYsJ+jx-G=hA>Mk`-%$N3W*|HnYzkbL#$$8A2 z-HeDrd{Xa9Pfx{l^T(Lp@c4^6DcsK)g(Y-pisZ1B4L|j(9p?OwxrNBjA7D&U4`4dY zka!;I0p^*kcs&+mAVwu8C*t}gQ%uNLyr%}wuHJxS$102v#As=xMNV_Ah1&hzdpmLW zvztt2dfQh=Vchrv3bSLJuU;@~_E;>Q!wB!B505^x5j%DquwXK;N8mhSgLrL*_>KDAa{nXBd}GRf!-0e_a9}n@<~p(d-6Pmj z+JMa;97L+qXX1S22HqwgYG{}D?LCU$yu1x7fB6Z(WbXO}!*KKR%aD@Xxsz1LJ7%;8 zhYyzEqn$NaxA_o~8fw@RET%Toj5Rhiql5xqzy4!<>t|bVtW;5z6Hou+Ww_viVLhe& zA$;)jRgZmOTF372Qx>OXY4L!}iWvC*d!OPv&u>HhF)vk^ALO`Dn41I_wRFD*-b$MQ zX3w(C_bnZRMT@2}D@``0>`ahmpEyxN!EM3MUp)X+`GNEVq_U17X)ZJ}96nmk-=i;) zn;1Vf1NVNj2oood=(658L61Y-781Y)7FX)nPN>Vv5L-If4!ov7rL?3HTes}P)(_bm zj@F^H!EeUF@>5+HGb9aDCJr|90K*GBW|$zH>{-95p^N?dOR#n8e)fhk>^Kd8edzrCneExJuW2K7vPGx01>Km*XF%3=Uv!s`66XPwEFW zvKk54makno3@dKE%9_TET?kBS0^=PtKa+T(M>Aty?ItY|^Y44&kIY)@DN;A>IPER7 zUzwdg4R0&vU+%#}_kG!}`2m@QX%fKYJlJS1#Rzr%wQJwOGi!GtEicgw6W?(0V9cIg zXoe#C8rm~iW_8%GJY;31*=2%+DB{^fu#>$6v*tKdUizWW}P9Sibyn zB(uxfG`HHLa5$VDI_{Zf?qa}V2|%NBzqZb!hoXIU5P?R94AP!k;g_2dFfm{5Q+ zX1u>x^NtDiB&gmFIp*x_R4UJ9CV8|`<4?s6j!nn?H_=(G4PJ;EMgWX9Zhv?k_E$BV z@%2}JxEKY69usbw`C<8kp363)2ea-)vpsn5fve5TT^t4Qx9nrVLz~c6UHf`@oU64s zsR?voy`MB6oSBhmX4kZmBA!qn-f>{1A3^j;Pi>I^Y~59=OY!p<~uMp59K1 z`L&c$4!4s$-cB^w7KVm=z@6Xz9rjkz3Db`L&&q`uci}J`FRj84o_-TPIybFm>+SHf zokKk7xciQIG)ZxF!X^y0B@t6})6m$2haYeC?^7 zkmf7n?HGpIIu>LlVzv%Nu(iMV&kgwLZ}%f5$8FXE-LY^4rcWJ#^o*1~5rVddmatno z6EH(80|#c=8J1SFqVcv4#;K794<5($-}xg@YYhb}A24e=ljv^P6Z%pwXoH%}d$@7V zP~5R%u3ck6W-!hf+}cr_*~JVcFolKr_|>BeaMP7Tksg}!RiObeFNL8cg?*9e)2eHm z>@-1#i#+ZIV-x5HY@LwNG@#GtM`dLlYM32sh@vmT;W4y=#)+zwE{ke=egkU^Q4IAIvZjvL-KZ*3E1Q2<+l0x-MPe$2zDS4K$N zYiqr@_MR6}x2v8GIB5U-Dm0m|z(LcM-T=O_WCXsx{BpbU0{PsD6hL*GjqBKn<&d{+-3wo{7R~m4L5Qtt=CtXfk&}~ZmO9@4*xNvc3*+;W z@o(R|23eV@vG@W#C^wk`FndXK;0T5A7K=I7S?mzQn<#*XzPk?Z7FU>+&AlEP8%}oO z_p6s;@ZcOAJa8Oez3)}?H0P-7MEvB5YmuFu7K;Ejkoi{yk2@^R6dK z1@hcT$xZC_q;}@!{B;aSCN(sPWK;j`z~c@AV9K$Mg*LLb6?#CsufoR6SEyih;&B@~ za3;8gyP_f6p!SCr)9O}wI|-puXz#adkj2YI`K}Jz&8=XsEpmWV&SxdZ?7)Fu-JU6h}KAAR>4WMrg7lMv08Wo9f$ z2NAck_d(`J0fkIblxtgeQ(!Z@dizGVI?7(qziD6eSV-;fl6T!2YF40K|DLkW`Vj%1 z?vD9FSe}w*PrHK`TW9?U1I2+r>x04LE)lkyEH3knW?0$3b#eD73m|Z5xUV<~J5~%< zxSXUFUH#}B;?43%-Y*kTuuhw%UBP?gdE!2b6+s?_jfk@){($z!*DDG8(GhxqgLo4W zA{KzaAJUs59=6W<01U6_d0%l3e**t9VG&9^e zk;M7k!4?yADuM%eXWQ#+@1c^}J}7Bh4`>fI^o;vE}&J)n@BG(dRsU9loe||g)dA1?LlE^X}q@cRYIo? zWaG7msh&*PMp6vFo@U0ja57r-3IVhSg`o$=6~0ahQfO1zJW?8|yCDw^5$Fv)CrDzr zLdfkw+D)ccv+OnKbpq%N#bv%!HZ+ILmCwi~WsPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D7AZ+YK~#8N?Oh3w z9MyULx_f%g-PuEX=srMN-AD)u2$sMmCI)N{E4DC63^+KBAqlB6N#Uvh2A3UIQs9)q zu}R7aaZC{u2QU^gm%&^{0wi=T(&_+o?tRRC_VncYduA_bubG|MnFT)Sub%Fio%#Fw z-uwUe`n`UwpbTN${zV%%vuRDVrL<+VxwKidYT6>2oi-E++J`hBt&Mh)) zJ@}NtZCRmg1R&{WakHNGY1;WTA-RH92!v=N&JNluv^OZm2i%q%B^7|AFX85D+I6(^ zXl~k6AVS+o`xR{q#W*YnOMxjDfTTOP`8@5k5Y($b)?8Bx&YY{|8?3OG>Ple0g^8sp#7}aHKM2jko;@8 z`5tYRbzu6TnRXXNcsjFNcoand5zTkr25ub z2R=IJrrkji{yVc<7!*tZ5-)A`2Welk4t#X+2<>(XA$^NUqaXs1{N>y{Mq6tg_~>Id z?N2Defy{3Hn6v;S|Fhg|rqx;pK0fHBT~85yo7v3|YId6!Nd8USJWKnykuQ*Qem2WN z-pIQK@Q0-*`yZLTk3DW-O^}{+&d5uEPskq;d5Ms-0NE<{Uzxp6a2jr=2tUuho)B^p zfaFWUm6$Kr>p#Jnknx5qb4kn-7J%eSQ}Yel$1&mN0%;wsrw9{jqE4t4MDnGRCGGr@ zkT0bquVhsk2uF`)IU5%fZH7t@w`83xBb`Q)HjP9Ou7tC3F|GhqQm*Zk?DB%9DA?&s zU|V~T&Ze_Rrv<*2AKwJAk^NtdmZ{ug>;*xVUMcV7tL{xre^Sq?|V zcr`#SDNp%T&A6E%nS~$0q=~%|16vFQFL%cg2}~;yggfDIRE#PBm6O4gl3dS!r8A6B zG(iCrON>6+T#E58Ol)rp z!fhJ3w$_1O9@qK4M*B_>xaUL+FSG`6B%HvBr~w~Cm5A3bNSTQtTneXW;0!;hl+0d` z>)lg6fG*A$B=Nq(K5PyqQAaXws8F%U>%hYuG58sZL;yjApX;G075t2se>mHwV7XJn zx=I~usvMZ-vcbmpQson>WT1a7J7~jZTZ~i#Wa>ysW-ka;cE*y`6*dI8qm$T&LniKS zjj~n@k}q)cy?Oj>HMb(%iDVj21(NvY$p|(b@Zo{?{b&g$tk5J@1`tk#+hG{yS~gC} zjIol<+mZ0m5li834*77Lq}uZ!j}Z)A6wJXIa7Tj^8)tb?X;aIgHpslAb*u#gQ(aE4 z0WxE)#FM5JD-XCxo)DQ2?Jha(H_NpM|^0H8CHHMF;>uo;{lv5fL!M<8Km@g z#tnSu{Q!2wQxqZl#%V}Xpp_E@t2DT!#E!BhT%sh z%I=|+WUi(-l3>{Q=MMsS#FxN)5;v66Si(SGBo2e0nc;Dww$cTU%Wfr(P&AI-U=%&! zIP45HHEuf``rs9F`R-HvyNzxQcQ39&h1~|Tbm2(mqOM^WvL*x%72sRkO!;Mpen`oD zwj+$M91mlSRD~>jBsFfNuzuwNT)28M7B85MhT1B)T~6pW=?Tzx!$3F^Lr0GfhmN*l z``*LY)^Y+K676*lHop(Bo%+LC8*Z5Ig-U@&sx?ytnP2onc`;Og*SOh`*_&EOE2Sr4 z;u{D0aEk3vC-P|fQ`aaZEI^m#LNq*AFKm5ro8y44>RD{nBQ<%I{OzgO0_M z*x7s-KYsjq9PQ{s4MW#4e#Q4Ifvak4a93AC*IA2_3C6!L*Up#+hE{_i#~;D9>sR5< zTQ*|pqB+*znoP2}Kx8sm6EupjU`_-6;G8As=|C_{T51%-6#-~H}eEqnR}_*-tymDh!b1PmL(^VT4>b^~J9 z+=#^4E0AbfjpXW!fw{{OHB>yxlKRrNy}0#Dm*V!XUW3}IN*JkB_B-;22vJ>Gfpsg+ zLT7I;e)Z0Ks0sGqiWSS@y8J6hZ~P0SFTW1yWvhY3%aK0&9K_=$e_Mwh?JZ_)@KJ!L z6^0*aF1$;l2Ze{4PQq_rh=Bj^SzMnTs#IgrRLIwCdVSN#}1R5()SMF&ynlVON`m zIs5nF+LrsExn^YZO~D}{f1L;*0|xKW3Ux!=FsSJS{EoA6=i+apQ+1+7Gq9w}z{*Ag ziy9Qn@UqG1urpjptz?Nc5=ch_P@`QmmcUeuN39H-*-jL~8i^MX6{i=9qXw$I8ftAl zY`V_ALkb3Ce=roo$?iA~w5HMAX~Xe|4L-$=OP6!a|mGf$o7?Q4h7UkpfggvFTQ8D`g}R4#&{m8^fVC6U`kO-Vf-A zm@0II!IEDcQEj-oV>iCAGLhA3I=1BF^^=cBrJGWJlUj3DZ-Lw3}vgQ@|9I6k9Fa z4hSKni)jpN6^+p9m%vuP7@Bt`G>7yn(}=QZda@&kT}Q%raeoS159`>`X-99|28lv; zn)t^5Nb-A^hTE~Ubt_CZ43lh4*;orSt>8)dZ4p2QjxWgU7ajuw%*Y6krObkqNGWl0 z;sGY&Lw-BnZMWeN#pvlvu(T%N&{C`w3dL|B?X1p-!MF@XXKO`9#wdurlB735YiNS4 zW-e^b3TmSu9*d*3D~xwZ`t!|6Y(1!9d%GQNQ3)x^Fw%!&r~qlL!fJ&TNrVqXfN$`H z+=*Tkj3vNOh!|2UqzxZPiWoX}G8wioHuv}pq!~IKHWN;MNU^(MiU=(6S}G3PJT@OI z>!GntnMfqj-V-6|5j?v$iC?!U*w(6Bq?=jNr3P9lWfTk(K=oK1yg?CIiwh@0tMDPW z|CbGf7a_={r74JlwwMji9k=75w{<-9N*YhS6UV6zmV72rNVu83nu53yk4Ev4$u0Q7{SV>( zAN>?Byzm>V#pGgim<;*jVW1}Xf~*O~WNX4A5#aPyM~VVz92TMxC*P4^Pu}VAqPBJh zYHOX!^MUv#f8I!675bcx`T3(Gf8k(Fm|RYf z954x&$ACD_F21qy9K@HeK*U>*o|p$u99xLyu**t{xq`I7t0wSIP2Wa^`UXPrneg|9 zcoG3rIX}dX*kkxn;3{~P+?NH4nF>~UqWIE+qo{F3VFV*^z5Oc8ZErI6v2!S$g%L22 z6KdqjYVJ9qI5i;aMEtVL5nKN$r0Z(o))QFd?ZJ(U522AIR}yNzP}yTu^0^a0hU!`k`se--woTc|fumfJooqM~0c54A!h&t~#HThO zv9u{8hBaoZAZco-cLnjehAu?&5I~%bq!he)WDZVstwA7Ek3h5ro&GxPIqJkar*ydT zC{p5!_$0oGkJ4Z0=UZE~0=BEaK$b>dvtttBC?Op&nX6K`3Aly7%KEQ_s{f5ZK#9U-3PR{QIWy1=AjVMGo3Xfm(JP|DKE0dTH(MBQnINDX&W6K zAs(7ML9acES@vY!VW>jSTr>;BHME5_MA>t$b)>D%Tdoiv#Fr}iB)**iIR~DLcFt-m zr;UZda3nQhLQPbk2p|jG6$s)iyZqJF)>Y3D!4tNrDOB26Q?iHhft;E~?{y|~jh3I3 zndY(hB0h<4BZB9m)Yh=k=*$Q*1_ni#P)=wQ0c3T-0wed5SV~OH+limkfC%H~gp(qG zT#6_V_(M#U6eUB*FXEHP&RbPm zW;tJ3QMPc9A5c^{UVRC+-@XjvR0jeH2M+rh@W_!SbW8j%BLT!0@kx9;;;V;0;YM#4 z+w{*o2jht+GR>{QlA9NXiI53LJB0hJ&en3ig@rh*+u=U44~eIKj|X-y!}kv@!{MmY z>iCo!#3%9X2M3$*5Bt~RciSt0ZBOzIhgFygfk6?1h(+TT=l$O-Ye2|!xq?H{9dK2; z5o0UE#w2GiL-P9}zR8$_-^dIqC=R(>YjP$3bchIM%8dAX{K9!g0GXr6jocYi1Y`)8 zL6X9rN^u}Q%0PXQv2*qlZ&-_m53O6Wkah7;z6HM&7~p!S09$Bs^S~!4E`(#@+QNVU z3`26qhO8p~35tuC8#li?uw4Go8X(AP;kk(*&kLU=M9~}`#_{O_u*YFd)GZ)*A~L?p z91v7()}*J&B9v4Rro~ek3-WbGR7H%hCj+^r8dEg-saaDXivYPzRX85Nue*OgHGLrgxnd!Q_e*Wxn5tPrEt@{7ctA#3kTbb zo^%k4y&-Ho=Qx%vSdWA1Vm5bk8<3Gg5ura$)4;i*HvI94*KzK#r=c4mmY;%NGQ5}J zXM6Uwg)@qbQKY2`G&6!oum?THe6%)Risr`iv8!qUTJ0XI6gW(iVvOBX5&@h@;NAuI zVy>4BiF6%8z6g@BAYXUm760S-g>eNcthMvThC$L}QaD)Bo1(oqx2FY5+uy{D-q%Td zJ5p*L(yEi6ELii+TB?8o3|p#T#5zk6{)(F)XZ8wTh9BsS=dm7eY zU&Ufsz$1u}tBI<8PS8og_xaXDr15PXm0~nPSMalqxlWdlue}c=#yV1NG?Z z^6_(hSXj3m?`x0Yr{SwmIb>Rjl=L`FM(E}wLs%U+f#qF$Ft6ivc*5*^DmqNHilK7k zvEBlqnGT%a`1osQjO>q^wJ3rtGE}tAnTQY(5#*W?30BQ8&5xvQCdFXQsK0<>EVsny zwK<{lGa`u1OyCCnL7da@dsKOnhz5EQ45ZNMJBi=Leu3T9pQT`?MY>Ay<|c%6=7+Am znA5e5r0*gZ21|MkNuQEQfr9H;`bT^U$EX4bD8m18Q?ymHhKtY-mU!k15yQ|LEHRFW z7{rdCp>*#Sr zjR2FkiuU$))_?%~%njF^wEc$>nKemdl-8|P7gQ6GIWrT_wp zAa~H@v>tM<7`Y~#jx_T{7-l+(Ewvl)uDzN~M-?Y2Ks?~bY)!#m>fKmt>p)VS((p1C zZX)?(KJsK-0R$95dT+&BJ9ONdFdUMSLz)|5js1y44E9u<$=QTkVTUjCk#fKzB!Ar1 zXC~U-x}7F{gW`a!xjinpE8UqP%yA$RRi#3h6Ypeag#^6r{`HQ^SLpL1(5??B6(FeT5CnI<_w$j@sm zCGV?FtO@diH`9un6Bv%lxF*QC-<&V`f=L%E)`Tf}kXsqn1ZnL^!Y$o{+yvRgEYE^3 zUO#~-vzj31DoxEvkuR9E0D^22RTlh{2VE8iL;{ES?Y()UMLo(On`o5YlO@@lAiQsw1O-aJ_!+IuBJ@Ml+M&v5?|C3e>$dI zfT5rmvSPce+It;M68EH*PL5K9@We!rdm&_~RVE#5A?X8iHK!US6<{bRhOA#L9lB4` z&ZkMf$ayW(XgJ2?e8#;dazmWNb(v}-cc#2S(yjUVrAFBZFccI+Ch5$kHPK`_HCa?} zE=|_AtEP!Krw{tdq9a1GFBAy3#UinjtO;@twA_Y!h&{R1#?7x4^}1E=0RA5)RW@)S SVlaCE0000PppRsM*wr$%scd#=%wr$(CZQDHW_aDwhlB!&$lCE^85~V0F0S|)% z0|W#FFC{6e{9oMr&sv}${_}kwL9jqTaMo5LB8pNXB1DSL4(3+2W|p*vuIa0I2H=mMWe)}W+_@`uuqRIkjG`kP~**5=fZhg(35M{8)KyBRQ4 zQ{0p=aj6}IK{{?|c6r`Ub_6VDzDiBWUkQMk`0t7qcvW!&eWikEiFwnxxaqQ`ykU?6 zftkO62n^`WMu){fDM4R*Cp&THa3<6&$W{eTe=E;mLY?`6#xx^Yz@7d1hem<>D&Lf~ zfPqAdcdVU*RHH0JswhH$g}bDq8r3{IQ7kI%_Wd)9zz~MgfQc48d9~PqGG6m?w8mz` zc{6f}-BY24P=K1kh4A_?@4=90ObGwx3MfTQ61POWC`TIc2ByB_TfFSPu|U3ibzKQn zO;j3;5gWI&CU89e(oX;W{o)-CMAYH5B+(KLW)9FC)M-a8Oi71uN&~Qvt83Xr3Pgfe zbbtmbi$zD8rt(=2q`4S=2Lcb(fapu24Ur>7NIe)AEPqVVI$>cNx9QAL9e~)Q422^x zpiPLNF0sQ>*JKbb(xQ+R3(JPBbtq<` zfYy@_(LZbrQ8rJHkswo;_lcKCDvmh7I|VDE5L6wE!Zey1L0f+&`MwMGtcm|g$1NQB zgb|fOuJ|x4ee3#pGooKJ%MZ=w?ITS|h$k2mBZ7lgJC5hWluklj<|Qlw?}x5v=2XkT zENvO33-j8S?j{=|co-0U*5)k+vX@5}$RVSPlu;gz!aTksIaIyhn-pZ-3q|% z#x{MP#dxV516LrPfm9+w^w)T*O@i~50anNp|3bXw<$Pn@;pI$u{M8r8`z|pOk!#W} z#JU?>!l?s1;i+Q&lf`FpZ_VLdFA%vjr!L>orhg|N_ zZ}H*pk)1)@3Ni!}0Yc*$kw}N|>V>k9I6}j~i1H;_lLCu~u_f6_gwV!ZO$n5vs|2YN zR^??iD3U*l1GXTZ)_8{O1(h6xKZmSRVkBMU{^Uke_}PeRI|ii=BLsr5s}X#OrcI; zU{ODVkp0L#>n7FF(vjZ*vi;tX>85bOZ*B-j0};`0F=b+Flx6OEhWvc>)aK3QUF_ZY zr2X9S?Dov}r1DgHrg1?y@4(RqXA9p3;RPWB|AM{6_{KWq3gr{&I95RHCQ<|E<-obb zIHqW+Y23DX*RfAQTB1~@R5R~f2vPVs(pF#S)OBxq_&ib?*BO2R|BO39sLkxUJI6y_ z7F8$}JkFXtO-@J7S7KhWU!uI&TtXq&IR!TrJmoy4{tut+lGP#|GCgKm;~ybQK>w0O z!#}Sn=MyUP*03nC(69mHWU)%I+Tv`phGX5y?Z2U>uvEYtt4b1sSA&f+8Sm^?%mBRTBj?R{INTWxM!Um0JuZsKlg`LcLJ z2`>5Uy_k6=yy)+2U%n2GugM-O9xt9+US{rU9(u+XOor<*8fV^-e1rYW-|=4F&n}O3 z_Bv-@6$C7Lb$jvsXC*e`(Bik4AL@IH1!e=11AKs3h)4}h1byd+FLh&OVoAx0 z&_*7Q)WmiZT3~We-6--h@zeJ(|5yz~hMq%Ppkha%qIdv$WMJgAGh8{=`L+IRWfi0{ zvlo*e`&mm~L@&L}4@%5p`7up%%4Myj%S$Ur?Plz-kh@M@FyWino6RG*UNa&hFRl_o9J8t3z{#{S2TFSZMud z(KO^(d|K2_-JpR-Pl)On$F~W8aZ`|3h|GL$t!>%0`+1dnbv};2W4}X^btLJImq(eS zmde^Db4$e(Unl!gROc+w#U$CKl0G;+jDYaHDeo54X?zuVYGNsBYP}gbPaJ)#xRywk zG|9SUby!|B7St6OlOCO<=C`~>vr$vs*Cl8-7#{XM>>#Wu(fH>8Rv_W6{p+-cOiM;P&6`fIH?~c-;cDce?|4(=tcp_m-t)LD^|>;=Qd<4H z*?ZvP(TuZK_<3S)Vq1+|Ev90p7PYFgf#1?^8)6Z@ z7?eta1N#q%gfFhW^=8VfK9-5Eqkvzz9AlE zxx_{3iU&s|&Iaxx&L)C5fvx?64Y%Xi@m$LwMg&d58KIVUinr^}e2(aT4#lTD!?1Mn>beHpi_1H4YYTGKkenR_>JL&sKb*70!%Pz{6w?EJA z%Oy{khxhGFk3^e>v%#nTI%qQFaxc$UPv>^!_t;=!1YH6O;kMsYz}G|6*Z67tyTZA` zWX?Z5%eb=s(o~vu>ACc393XPXq|8JJF*}pgQ+xWF+wVyk*{x(nU zn+5~{Y_CJlyq;;lHLrf-kcEhw0uB5*-zgvGTDM2DMYG2Scs;CsaUb5b4864q6Up}< z2H=dGb5*oJJGP*KIS@dgFLp~z75a4sNNT?u34H+r75?=GT|i|ax3~9tch@K6Kt3&f zFR%|b{rYV22!ct7g4^4W5!y#uH}=%@I9DoAh#wz2137U`D1i)*=Q{*ICzu-R*yRQ#iC31Vm6E zP$)$JWkX4ja;SMJp)lcvkQ}syLSaPX$P__k)?g44us|qdBMh0U)m&wbicFs0lo?5q{eU!2GsQx4Z&DeTGPwf?!@q>$Fg|2G(=CyAB>X7tb3K zolKE-bGgwF-P<8Md2_d~(M098){K;aLk;B51#tcgUra)43LzgKM66#Cy(;DQ+}Rh! zD(4tb)huU-2e#}Qv!}3Wg}Nus+{rx26UV&-X3n?p%I9vprP&|AsD}u^H%Mb5VL*}8 zn9q=@{fiilkc#We7=_4w!7I}J*8u?Jjls=f%tw0PK}SJ?{ZU*8QWcj`_&xP|iwBu~8KGai z1`Nz&KQt0RX@g0%M_C+rM2=CWj@Oj3{A*S%air3z@Fh#04IoRV@wG{mrZZnSAJK&M z1H$~CLn2BH;>j3Bw2edaHw=KwJ9+X}dCRs&xJvYg{(x(D`s>;ET~ePwcMGJbXKRSh zvmO{7^6CmIxd_hOR0eDG=pz7y@B!qw7sP8kcY7LiPPXRG5+Z+k@80;c?q2y^uiyKs zX+5`%_?9ZfKA*Rlx{!|ShYcJB6xRiJB_2V3@PyslXmiEj^Xw_rmWD{;_lN$Pz5C6l zf-nZ^czWLaKRlm02;ra0&z*2pf2kqRVY zuYm#ly1WLb$d-OJ!TQ|x($yFmF*CI(s$+ZGhu}tV zybNSE%u-Zp_$P}yYyYBBj&8dU=gRIumN~93Yr%rKa6WZRncW^0S83wXFSaPRhDB&S zz7Dx4{d}PJt$zwA+A8Iq{+Rr>KfTLaU5B(QFdh<#68lKZIa>l&M!g3|Yw{@10n8A8 zWwT!EBpEWkOI9iKNSP-U$orDglIbdFP2OK?r!}>;B3exlJ{+Na@hXvbri`D^pTHbD zn92tma^B9{cRuR&ZhGVBHxGX6JK*T3 zUL9sU5v7Lo1(OU+gM}=bXIdZr9gL(t)Wz;fiZu0+u$NxN{c=2)&5}RWR@B)@41_PM zS#@cj`e)m7x9wWt>C_SJv($gQrbUdd78T5 zM+qAX5~(ifdt^c0{gyCLs<0L`E0rt@z*R#SSO$63zr^e;EsHJV@E`ONQ`=du^ms@xv% z8ms$Seum2Tvvj}+AJx2V2FN)z038KozI8Y=7*Sv@}Sn{795cZ1v$!ed}seIDDIRP zScL&!`zFA)l7?GIq-vA?RBxjqS9xBo)01*>J%=h{ry^g!HJuW}<;HoC=Bvi4LbHpIKCIx#D8r|HceKcwyZeLC0ce)v*r zym9mEP8Bgv3ow{+o_UZ5b$9Z_eJH%*T+b5w-k}t22k}oC61Np0L(2ioJn0q!qazTp zjt+geh3x$WjOVHB`4PVKIF6TV1|SMt7I1HhSmv-?8ncIZ$B1LE#Lkj}T`$%kH{yFZ zX~@X7!MiEVEd`PjnBNb$Bkx@EHNSb&FERTpd*Ll+7QV0cA#;H8wNbc>DeZ4BTQV7% z4^I+axVKM*93dgLf8e%O%x-9l<7wFzt%{7X(Dx9n{+{TeV!FXWpZFg1VEo5xSvK5q zChd}-*n03azv;HzQ^Lo+nk)Hu!3ZFomSovy0+CqW2RxjI7YGIAz7Jh%9Ko%ZMCuuC zAejcbLxd_9E?X23xT_7$Sak#tntwDq7BgjR4DuvPkU=p`oTVu0IRM}$$^}zLDtta* zk3~PwE1O}x$w@DZ95$iE5QwvaxDJPlJcc5Op!nnxapxOLclh*0MJe6q%B^V-zq4os zx;;%vRL470n9;wq$+O>;suha88kzxy&`8v#&^MRuZJYkoH|}}b?))3)P5kkm3j|=U z`^B7qx%?7fL}DLqNGvOg@Idp#fktlY%9!`9|GYcXRr1Ou8yD0z1t~90+a#`4n>Uq*s@(&Mp1GpENPbC#-9#fZi+IK#dW{Q<=-g+KLR z@3}%GVk&y|EhYRVtGaF%ZhvriEFN%pfxKQ_C>Oql>?Xc5lqWTRRjzFRvFTR7rXT`7 zr&^hijlr5nfO3Vf#yi1Xxs9DMJVasqqlhL}laIy2>TsmDM+kBsgdQER1OKBOFW0ec z=c^*we%gB5>)Wc!QyOz>_6#^`e(r$MdVUG2RKKz4~Q;zjA_V+*=jZ-`17-zm?nO-%+kGUen0R6!jQF2wG&(x$SJy@1zs1f&A>W@LXmCMR(w=?YfAD?uHebd6t<9uKtJscU^Ovb>JFQ9V^Hl zZ`YD}4Q3#6KixRnx5vdt%0_;d*U###_}rgi)8-||`UUbANz@Ar1vwNeS1jxP6Q!fh z@9gHB-8$E9**c(c)2?^va(FR6Pw*KkYOVfxvdKN+-u$vvrv)*fly4CLLYZwoG-e+Z zAiUSw_bY#h3lj?I(EYp6TGle>C!-4xfuuLTlOfO6>PM9&>l|H03s{Q~J$wF90c*&e(V+Q7W10#`n?b*woNPhY@5`Y#+A7s~-UtZIH2?KQ!I4`tfbl z`hC~OeGlqx3w^2)rB=*<8UaC5BPb<64!vi%PLLkB;@L#W7#qoUb5M+VDmDtT_2Huc z+~b(iS<0j?E-BhI1-D@o!i3@>Z7eZ$u9jBZ^94-vvwmSQ!ymX18bRmg4JX6_m$g7l zHLYH%WjdWw?16CsrJ~(Wy#+*6`mn`*673$gHCO7?{^BkD^lsa`=G?yWkU`sm(*K1A zU&mK7`i3I~AQAd}7v2%x7?phzBHLI98k2&o@y}0;3?KCvKoRCj-%3Bat-AB=t?p zQjs*^5<^6#6>+t15Fenbwc(s4NxtOYhO%(`%p{`7eMMQ@WxV(iK@#p_1NWjiq)OX_ z*3!aK_;w68Loh2#d4I*86RDc-lihP)(g-WBE6yud!L~=GFmDGXA;`8 zS@*8lt@yjo+3|$|-LUe>V4D(^AtdWJ^+lmroeUjKO)eoD1p+UhFDKbfkETje0sEKH zz89jVkQ`|8j!{KI?N6<>PO;PG*X}*l-(P101_CmsM_cor6pfM|x8QMI2)IcWxD0)+ zq>+D#kl7VPZDfA+dYHrNbbb;4cscE|@8_+CLn!~M7S_L9*1h|)qQ91O_08;wghQ<= zYWDV1w$Zo!7zW`Idl_|9t8jZ8>LLU@U^4^!j_VpXV^{cQN;rM{fORv&BBiuOT8fwm zNy0~5<+xhmAT4Xg)X7}jb(WVTLwBEj^+OD~`DQ&;F=7Gwo1_(t*gY zZQCLIU2ZG6B~>|6_$Tau3?f8|UUxyPG9<}=l|KmZS?cQl zL@w_x-nImkw0ArqYF9s(o-dO!M@R$D$i|MJTHduDfv&#$EsH>9Vpt2kP46X$!brh+ zhdy$h*+HwFDRm&uz%^KG!b7v*Ogn>o>Mm?t6$_ zmDOKrz$Ni8Nj>FJmHpiw`MT(Tp#SoWsMayWF0FnV%N+SU6c1Jy_C(BrjvU0f{~o_l z?Y?BmUZ7n~u)JdPhJV`}&TqqNt<=wk*vhtZ%PMb;y^gU>s7opUhLen{_mjU4)u=r>un98^8#K zw9_<2Bpz?x8J?7K!Z7OesB&%Yg7{t>FQzs9_M-AE6*AG|J(J_A*Us7ExzXju8CMaZ6A)a39 zvxo<%vV=%1)vc}K*aE6$n=He5$%!2*H76avZwl%@+x>Bu?S}oy?U(o$XHI|l@hE>$ zwW)O1linbzt$P=Lish?c5cjTq?dQ&gSqi5?U*g6s^8RYgSSv1nM;ZBUNJN$GLItb` zf5bb!#*z-(FgXku#I|6v5pQ1*)nvlL&L2z$Wcpuf&&Z z+1dC7bPRM7Fa+-`X0u}GZ@WRpIg#b$2Hi6rn48yKH+A< ztJx0`b_K8~rt$=!XFeLMKR#;vMmFW*vs0%^ikMrC1Ye4@@QMu*okdOWy(Pqn+spI1 za;-D+EBKMXBm15PvYCZ6E&fJ#)S-4bK~}(X6ve;;TU_mRZ$emvQiI-1$d!Hwe}=LO zBS_(fx`S3=B-+CA1Ph3n_JMLazvW*9Gn16tsFsNEX?r3>B|a{7MMeUMh>Iu}mhho1 z>1X&+C+~YX^aL1G%Ho`qh(M!bblCbh!+;#|ei_Pb-S6c;y45cM zWTN4ss1x;lT>yjr*da;ZgdvI>kN_8&^{;p7uVPQAebG1e4S1w=>!z+QEWI9QvJU@m zaaR{7V(Vb2F;*$fEg8?ZeW8<}W6jtZF5gSM3H?$D6T zC7W3ZjS9)*cA|Pi_|S#EU~&mLa-6ors=^!Ndm2jWQE~h=Y*Co{Z*S5Lcw8Rz`3EL`l4o!i5G$3LSIAkI@**S$%t)}!)wYA%?kPc$dWjXt=1xX z-rRW;Y;`M!Qg~0Vq$d1Jm}*;CdWWbR!di?*TZfHAM^`s`MdNRwSQ%?&r40$E6z4|v zUXvv{a4H#vl{)>shxZ_*%^6gkg$j(Y-U^mp8B>D__jH*`2$&wwz@&Tu7|3X-kpMJ3 zA0Hp~bw*~w$;oL!`}%nqHxslmolSC4d`Al75tCIJ8y4y-41%<;OPop=OG%^Ccg6M# zqfw#~pK9hSclTh3_=g5d*p-i=f3CztMk6mjtQuVRihCdtrD+Of}xF68vAZZMa|4P^>OIZ5)s>{;|62vF~nzK(He1 zaGY@ctmdfzdP!OputKXw$o|NXP&~kXNlIccbsw=4qB{KEpGxTrkPz*=l;u@J3BkB_ z_iJs}=ZR%YXmMUXQ&IW~RB*}4_IEGIGAi4zrgaoG&wiLYPoZjJCg};SyU}H#(-K9U zwm4sP$SvdHI$cn(P)iTZuSZX1Uk5YwtGix=r=3 zG`o%`IA)_7NZ-mtA^zwnH=AUy_9OIF^wy(fPrnC(MY)5ox+EM$p{cIT<9)Hbi-Fnc zg1(7BrBD-?mB-=9^BD=$?Zh$7o1c(7mfD0#0)EU3D1p!G?Vl0FprXnCODy9&VZSv; zj6s)hL2?R!MJ5b~c$@!zw305Pn5=Uqw>5;2VweODH?mKxMdne`5bMB^{Ubqs@vcB< zFGZM(sx^V?3@`d@dMYw4YdxFbNe>V176-7fm@tG|rQ8>CVSfQ-L%U<8YRhb~NTzMm zZ>LjjuNyY7)F4g;TM@jG_;Ql+&BO8J#u2#f0}Oyr?~JT13Gqq*X?ki#7-m|KFAMo0 z&8SbM+DJAMjDjLIK>)61^9_c3vZgOL3dWS)c_gc>Ls{(HXb}g#W87w*I^Zt`v0MN7 z{%|LVx#Q0kmt-FdloNsbbpKgj5a|5ZjaQlGG9b4h#O)s+r*5y6`%D}dlgAVo2E%|W zF85me(b>W^kIJ|D;W4L107j05y)c27KiQGXPujKiVe+|stDPlZRB1Ti@#%Y_X^s7f8Hm-(!u9I zg)h8)dbHS#zE4c*BFNN2Yv?!$LVWtF+5&5DX<)+(cQ}AIP*PKQTt}@7LsD3R(vfnp zs_EQ8rII>TKK4~M960Q0f$C>PPw91sZddmeA`8pU%g^cyC5Wv~?i+CzEa<64V3ZCL z7a_NY((ys7OAfGLd~kVC3fnSyqD*!E*kBbVh5`%)`$(iKstlnP7Z<&iF~$8N>S??; z1!Grh^dOOgxwhJXL%aSPe@~{2%uH}(g3u2)zy_q0AbNA6SO|MKHP&P>jNmfk5Sv(1 zQ~eNTtQ2?;18&?Sb87#ea1_(PhVhp|r-1WS`$_ep^|_LjI-%-1+y%(?XZi841jK@r}*pDCwsq6^g>a2t)~= z*Q=mxj^ek>VGKLOH)H1z!>Qk!hTqKET|nKEq(Sc^Q(6qJk=dK&k;$$i5ta|85e&!H zX`a`%-?uCg5^B=w>R!U`ilKHlLQ;9;=KZ27IIw8@s-cs^|$|I620D8Z_^eb>oD+jjaMH2 z-2U$dW=ZydW06M-8B~{FH8bxYkA0c9{!w7N-h5N(jy3ZKK9Nol?+#M8JB)>D8xTp9 zaQVc{y6U#^(%k?9cYOpWLgy7Csj?j=@}f0#`xElXOeqql)Syrk^5ih+V2uSpH4CWo zNnRzV@FhY8vyj}Pd?q*L(Q+f)`}utl{4gil9}Y-+h0f#)hU=VBqhH-_>rhGb>+IH@ zc>0`PG<@zh0q&1A6JcF$iZ@wi*u@rI*0)R~in6E=Z_b|TiIa);{!h&=e{A=)cNZlS zKf3JgMof2vCA|kLMradMesLt&)+zzdisS5MN2>C_r}g2BrL&?IvpG@PhRmJB#{>ai zSgOC@^F5%Zq3BGKo9f|4lvDkyzv6EZ&pJm=DR9Zd zWZu>L{ZOcFtM^tDmJdxTW(`e93;jV1Qb}h}q-#61h?3yNVsR%*AVhx#iDWuwQ=~x= z)VRRy{tbh`rURMQJzcH4%x^k$qh}BDgcLO86M&Ks@_7&O#@3y~Dcu8{NlBtz@nTP@ z3jxq%xgfUy*_du{v(>(X8@t!C4Zf^OhYl2f{6JBCUre4wM!doY^K~BL`!th0kxcsu zV%K`uocZ;3D>E(Kc6TmrP%wUvAK|B!O$QOaZalt+v-z{3dx}EP2n0(# z)DLcnl;K_5sqV215n#Jc7Jtq)-iS2@&jmRlpjs*&x1eMu!Am%LGCn-AsK^2(aM*sBr1yBLO}gO-qDuK_T&i8%^vAccZV+h43D~b8cp+FNk)M4hf)U1|xzuAW5=PQ0>JYG9ecfdu8ZP}{67dVtZ z7q|PB=hJ`nEZ|GjjJA_D8)|gG-aT%sb#ncF!YZu~ip}pAD%g@vcE4*5-jNli5G#fT zNmdU}Cm6MS2lh!L*vqVho!{K`;o){WK|Aq;kFOp-TerxK$>H}B7IN9}$f6kpc9s7V z6;F-?hI3Xa|JdL6J9D|~`^yyp$;Tfh4-I*xAJ~2CuHC87`>?Uw>-2N*(bEmt`}g0! zu7=ySi7^W&%rVhQ{&lqg}9Cr}+`UxFV|_i0rI8+CXDv2M(u zpy?G|aIi|IqY)2QdDRu8K##NjJP#J$-J*RUlyC)eqz|i5G6X@A zp5Gmxd7j(e(!J+6n@F00^{1m`g?@fs!)EIfBc|~)5+}|cCpd$s6MG?3`eE!^9>b!7 zjwS*<0T=RrDS~cpRz^(Thd#C94n6PkO+csf{A(Vg1Of!vyzhXdzo)X?10|~Lh!SKf z#T2QC>O5#Y0=#dT*3PhBy$PSSY%1Ammea0zWif*dcP*JtC;qz8m)8pm!!Z;c&m*!{ z_Wn9_Ft<@aO3#TGt(WLWSn}2SU7lt0<$P*WMe#tgX8>VgCkv=$bYx@215+mAzbzPa z#3dd`@-e5m0sP(v;}pn@uT4aKnW^hA12nWok+ZsT$vHwD6qJ@j`xOp-kYVnjeP07h zQ=&CWkyiRIjy#xsxtWs0Y54uX#%N5;{0_~f@$n9qY<4SIC@n#*to2wavgY0&!)JT_ zpbfCIL^&_nw>>ZvOUh3RG7JMI-m3R9(g7tM>y$qmroR4G`6yK(T097lA@MvSb^am* zHK}mm))YT&Fck9~J@0Z;hji;HqX~S-epe=hVH3VsOkG(Axyo#24oPpaYAvgS5djTX za;v}JxB{NxN86>KNbD@A<63@7z^F9+%dKJDKE}H0$Vibi>=Ecu1KqqU1FTC`sCom7$KKxnd0Aa6<%>z0jA$DX#iD;}$|T za7J2j35MQI=!ym*;!%z=2>hOuB-6Fb=DWUUHY3UO<9j}u3Gg^wA{}N9xJ#_pm2bIw z_KcILWJWPYpp`k#uddK&{rc5ZAQs9Y{v(!Mutqtk6!N08yq~eotmBliR@#nFEBAxM z>?89(zJ0key}92S1Gq-}vQ@)J{utmP^#~nz(XE?aO|GF7wm!9X$VAa3i<1_!c%K^% z-037MA{Yh_K)Tf9tEL7u9w!F{ldbJZej^qk?M)yh2CE8pHW76RP- z3_gj6EQO*6dg$9KP&WdPTPzb9UYMbQgS2uBCNr{Jq`IC&9n(Id8}U^J?X@Hz8x!t! z13m0I0t5jU+pmSSzR!~|uXw_~4`G=4f`Z>vsb6z5hYE&bQi}zM;9uFjyMsTE^614z z24zWziHSn+T`^nxH$c|jh1nvFI}tL0ia#u;`hLeOyPu5=KWB%HDxI)nz~{EnyFp;p$ufYCSa%AI}TO?MccR)#H!;kV*)KcBaxypz)xT(u3A7jP^5E zc2*N+FHVt8Ia6tHfUV|nbAPq_m#q0cX6^=l`;mErFmli14aA@k9hTIz$Oo-p1TUQAoPrnSiAyI7ZD7h^2!npJABEQ|X3! zg#&zcyO1$6WE1D_!}v^Y0yyO5RUZ>J)a03`!M%dnE?^U)2)a5C%LvV zATKu07so2Mex?fT+;Y8|rMuxBQ4^wg>@v%!HyXmKdvWaaFl%9hhF3DrIMH zN0Yfif06MFa1yL+^z-{Zv3=dqjd<=_qwnKybL%I2GUVU4iYAyO(A~TRcl>dmeGFk_ zeM`LtSbGr`zSdB~&;3oojAQGflAjIrmr695M=;QuQne(;t|&2XPn_q7?(~=p1}tc? zLH|@_1kaMpv?&;GeWxi5W;uz{c%ebLe91TGa_$S8wUF?5cMm zINGH^l)CZx3q>!3BXOQf8vgq~skD~r=bkZd3>*6FpjObHW>?ngq^>AFl>RFj%y4qP zTq7w)WK10fl2){HC02Yv6nwSRD0JK*zK|NO9WUG5)21| z_3JmJ6*V6S@@fU+G|!A?5?s+8G3joh0N#w7*q9jD(c7!xq@tLJky+?gSdM^h3kS4- z$J5YviuxQ?5uC#45d6&<<9#jD{KZN(9|_Xqe#6bZ;iT?6bBMd1h#2G?o0%R#jI?8M z`}YZoW3cEJ#i5GdsxKfDC>cr(fFRNKfr5no^NR6C8Yhwp!whwnCyn&gUuED^l|+`} z-p@~BR3B@DUTzojrlsEz1s2NT%V$tV%Uy1g^gs_tSQD341&TWt%<3S zR6u51Y`Oro#j`2a8iGO1%6;VtWVH@{zayH&X=g#^0F<)?hd@emKI;F(GGDXpa|fpe z)FHoT(=+*#JXE&*Z;ORj2(6F10-1*jdAT*1K>3-20w+G3keeMCq2pTiahrqu(7Z8= zN_NjzN)~K5A1o?c)6_~tXt+TgwnRQ$S58I-*4M4=ZF=Su_azf9RJM~cOU`7;f*N`^ z%!)vr@p%bpW5jSSoxzjZ<{BXnsSwv3i3K3LQtISj(KnCIN(XEssaHh6p^ z%G$(*Wdw~Rt|hK;Bf2^_7i-~+$L}PbM9(30Q@esQt3T5nr4vNb>_})JrNUS2@X6vXrE4(eOo(kGecW zxX4=gRX!GXNz;^`N63G9{LtZjB&SqM@D2WpOCxi}iR@Hr>MZF0922}`aGR2tTCj~! zOW0w{g#uO0*7d6OB{aZN602)!4!yR#)aqrnIcV`<%Hi);=@Iq`qy3gY(XrPnz)ae4 z91Y@PwK}CmuZWe`2Az;=+CCv{-`V+r2Y#;DHkVy%a}SXRyXilBkdQ>V?a7g+ai2Yz z>4&@bd#DPu+mHrmYlzxGdeRA9$Fi>+(Laf!Efnn)94Ny46kUqsE#X95ld-eX&34Ly zvo2x-!>ysGbjbd_>FXxpil9SC8hI7f6~DE<0+`4)mWZB1@&Ts;8{lsf+O5e*b}HkQ zsK&6s1NkKOLzz{(FBGn9P|wjLMIf=k5=oAC7&u)984=*0QlYl$Lu_;E2L(lxfceiM zBqS&23B19HW;l0}C9sXc!_5vjJ-hGxKd7kl6cEX^&PO_tz%2ml%;U zGwAb20Uf&4c??rHs&QsZHORo4?K2RyRSc#QX3tjr1pihy4y|1cVltI|7*j=R?3qwM zvw-cSF}ydZB}q-3nK$*OZZgy^!(bjb)|;ZTmn4Bsuo$gKn%{z|b}6YP!GrpW=}ToS zz}1-4)g2LFVp$ZDy)yeZM|B8*zM>-8IHgyB4rR)sdtL8Qq-*G5cD4lAE4}f^G$0Z1 zgoEgP`lf=N^2^Z|5UpCJ66Qh9-ouVclEo^>?nhsD&txu%_`%>q1Q|~bL;FfO8<8v1 zN|hUQ0T$Cfa@yL*F#i(0lH_z2SVx8gNU6>H69=EsL7$o(d!>F$$~bWea=}K)1^FAc ztuz?Bun|B|Al(@lP0msh9I8nH>eAfKHmn4%#-FO3Jo-E@_6R=sy{SN%%PLDTXIZsx-o_i0%7$9N|K{ zwmu42FCqPn=h>a@jw){c0NRcYibPk&ICiWFT_JL4pTWA1d=S9YX|#2e{kJeaLcw%r zZLUr8G7P7@XFM&mM#Jq=ZWxt~YO`SIJ4gk|84Bjj2Hz{lFW9&q$h{C=j5~U!k)EK~ zsFvz5OF~*V2lmWb!b3f2*|w=Sz7dmBHp*i`cWJaQ2e7GsS<|JnZ4x_BK$wf@O7JIYh^Co* z9X3lV;^hLjuskya12<3(8)7ejiK`7tbdkoB?B+#;;RQLsi2o02K$X8kBIrZ%7$Jyz z$t5XO#Vg4raW+7+gAQoxl5!WZ=~pjbjaF8dW$5#ntel`Unt^RGht#ezGWV(KkPz6Y zi7f`1*Jx6jjtX*8?!NU13Fsb_WzLN%9n~zVtaNZt-=}0Q-h!Roo_ z%(=5rOVQ^N(vd~Ody&k%AL(^$VZqku3>GI90}hWntzQgH;*QeOq_S?I`cI4T^ zsM~y0b{&(=)fdzmGM_`<7Hl`m@UfRMC9kavOnR6Sj~;zA+B3gbO_?Z(NO*a~d2(b1%!)7(pgGCj(+NgIV4n|>4PF$>#<3NtVK3(b zWXYkoq}rjg_!P*|@9(qj%Zg<+Ur-D&-1><=ZLolhvyX?SFKrI+B3zjbZ`I5P7~)0FJIEFhk*&D7Sd z;qMQYZO`z*BGz^K>{;cD(xb`Ni$~Cg;t{y@A3?%4n=dh35MsAFOP%xD#`u9i?&W;O zVDveU5lM=DZ)1GXs^j10@E5u#j8NB;AjzocsBBQ}# zY$-ZFNRdkZHKYisPkO3z+~mL%NARuAXJg!G6>1KfB`C>65J0t1mt&HZUX z{i0gRF3bE%Qj<=XEan2$6HMUn;UjSHv^D3eY?p}iu4~t?iCJ6+<6w>5LAIhm;`JeP zaAWTjy5KETLvG*%Mf=nDZA57>&Y%r8X{ejb0h~A-=-Q{IzynFxhb0iImK8+6nDE!u zftatN2^Q-XPoX`?O|+9Ql=4bfdldE^nF?V~xF_O8^E15L?)V1(yRF}uk~>+vN!(Dv z`>2M7u7TYn{_#<_!ygZGX{^uYZjV)=nC}7@qI8+S~(n!j-kNyAXHRl zXXMy|osI!*9)2XPZ9W`7gd|LDpxVsOxke8~IL>zvj3$$yT}qBjMwAe4*b~QzTj3uB zg%|?nu?!XaJlcZ1`ub~fu+Z^z^ynkeaa8Uf0|8Eefk?5S_P`oJnH@wzn|UC<+s6-h zuyOwl_U5gy@fW*BGBouWeq^g%Bu z=mhvTwiFzj0MSMQ2e34&-gx~DRAavxT}Nv_R&~>sdjy>Jitnsw6m2SNVYw_ozVRfI67k zCZKs3s+pHFfYrtad=f5~Tss1;a6X`L-M}oNW6z;&Vm(U(%CR{VLXG;4`JC(vx*t{j zPeNL=t;2h#-a+B*MW}M8qKj9~g8&akCr+M(i~gWIs#4z_bICX)6i1LzUS9#$R8CX( z=oqoTyL511O|*F#jJHb(NCE!7W?o4(cH#Q<==C?S5AVLKBg5!eIf+&j zPoZL;@XtXDVGo!PAGH9KIm{Rn9ZJI?{0)AVF=DuJ`}n3xn`c^glBj)dW_Eemwf8y+ z&k9@3p@V*N>;8{3zF;*n{;lUjs<#XouseCkgiYJBNm7s?1}_H?_5@LhQDFGO@A5V_ z0b-O)zuhUZ8-beth|&z$0UdSn&=9>{0t%Bg)8Un2zf$t9efK-xmTQ}8-IGt8h#q_V zs0gp%O^dWR6&Tt^hcJP;p#w(@NH9c(ThHe+Tgashkz7*PWQ|!_*fP^M^}B7ZBd{oA zh|s|^H#&ZZFVf3OyOd{196EAP68+Tsr`28TETlYD?4z(@pL+UfNk*PgAd(^Sscpw_ zd<(vFUK_^`*)Aj&&VjvA?REF{C+x`A-+-<6K2*(beB&FkW!cI4>8DPhMCl`Nw9KiE zyxjO;d=_dkcyG4;Q5i1GNK|JV%$zR?j~s!&YaN@w|JL;dJB?W z0*DOIVt-BMU7LFt!oJ0G;0CaI5B8@nAkVK9i+C-{f zJ;l;K_Tr1m38am|2CqC%jyZk|JxI(udlYX8WS?||^Mu%ka6X_3Rbw1dYJ*hdezLQh zOIs!6hAbfZ=6I5;!lHfIW^Cnm5RH0|q1hB28mH0p=zDLxuGWP=@`+EX3O%^AZK-!Y zpU{{9k(qbbhXXhYW>2i@ALQ}8_FDF=B(?%e?a`@Ty&tf z`#_DY2X*X05@4^F>9xBhWpeY95>5?w`QCG(G2t|4qfq`xt=p);!zCEyI`P>%la<&N( z;5}u{GC~}wIEG+j8+&*7yIyq5{*P);o56pp^?zT+C-yl9HmPR?%XU5_oL-W`l*_c} z;L(m`+3yfwa_feI$T@*9djr1YJ^3v=&kDy+ZYsgpUyC4%(*31VTJPu8T7vlU%y(j}0LErgi`9Nu$B{ooY-ZJ+ah z7VMYe>#SbSN|+8m9OjzZ2+o%7V|;u3#_6{~W9(r_H6cP5VqK0h?t9m#0=lslqi44RofPa+8_RM@oiH8!0u!=n z`1k>*d^`CBBlpe64+lN+;Y#!!f#$g5CpuZkBXe+s9SSzBEzzaqnCq8X2Nkr9!f1Lx znKaHe`o=Ge30j|=yTRX7ZyjigHV#(Id++eK?rF~We0-s&;x=~dnA{eoe?B=yjvX-J$|LjMA0~v9bsf>EvYObQ{KpqskZo zvln1rVcRrVQje$D&F$lh=W+vGzcTB6X?4+dVr`h*zK$GI9$#H=H08-?Piu7Pef|R zRKJg?LCMQ?F*eFVak#-kS?5@U7cLsx29!;Tl3?CI1%N~(;u7)j?+rf6jf$Z*#6-2> zt*e^DPle#NzM-4L_TG@s%1WG|eD=nH!uTUXR$~}Z$bsW$7*I`C-hxYNrGvW{Ch#=b zR!(^OPK}C80oK1j2!@uMj?WLp8~mDa&qus<{U#YLP#@06v+5IL2uvDhcdkNVQc_{` z^`OST8~m*>cW9eaQju_&Qpg1QWgZ`ns+A@5e&I1^Ye*~Pv27h7tr{dboN2?dn3&1j z%=IPVgyZLAAJgl|JY8^!CgPUNnxYENQZsdz!>fqQA`v5!;@NL?4pl1baXJNp7%jrq+0Uo3F76kPn9hq|CR@&EmG? z?#3lU6@$}bf`EoD{L~YRHNk)$L*fb2WB846CzXG=n37O(?kXg7CSXrAiOQE9vT2?- zW#^NLu~J&nkOypb)%YiA0na;e4U2NA%F&~%j1T0D`E)bx3jZ@dLa^#~+!B zTQB#%eknClqo{G1svX+)vvU|LA;|G_48#Wz3_-{+UK%d9cRg z*oUR1_JOR3zmB}oi^}6&LgF?%5wHY{Ufw;s_o5LJT2P=GejbF!ByN?7L1iwmWgudg zOhT7dM5hZmtfzPdmETuk@bs{fx*{0XC|Q6KmF*1IgVe=57QqCrg9)tT7=+heL+gJ? z;#n35qh^uJS9foLe?$1w85Nv=c$X8#G^`{cOd8?{E{#csV-p}Z0f0xDshxAcmyBls zfQ7GSFPS8wH#GR8g0gAzv5l2?z`Yv5ma+#1g5;(0H5*a78fuGWfv38<;7QcO;fUaI z(I{Uc=Uv9x?Td>G_*uqz@~cq?MA%^&5q`p^(r{;mh2;q(yC8td%Sz869R{#R)I7$uztdf$qG64W5wE-kj}A{N#hT?pY!oz>>~_)JdYjLtknmf%ekW<8wJq~w4b;XoktSE zMdY|rwIuShO^2NXbqrM^JSTp)lFp}Kmu2WQQOiU!Y<4U&;%=ev*%IQN1-cXpxI%1E z&^wVUfTwp}5OL`Glsc;Mhm4ZBxWts z{K{!gM29~u?M)(18`Wjq8(SiR=+>Za@bJi+JYDF`TC7L8}k{}8We+)5CEcj;D>gOy7k-G z5->1gX^PIZl?V`&tNR!d)4L^nQ7UzpB!qN-|;i zBKnX}?V*Z6)sluVQ@xl*H8;yzy;W_ zjl2G!sN1_sY1c!gb_35S`6;2#^A36~oMTyeEy=X(RdzToaq@}W?xR+p*3MaU-XyDaL)$u|5FucBP|A*_R zQbDdx&5F-8wOZQBhy)sX#|(RVI54Ht$4S8e6s0#AiHe~mG`7aSjpLhLB^)7%fyql& z;CBjxT#5`ZjT76*zPug$*0iT9#wI{(A>lf&;KAQ%dl@$nx{laWmJt)8B;GAeZX)AY zvb{*jlx|eDybq_8YUgTw#1Yn;YtaGmYX;6bW9^QxvO zqmvnJGc|Libffx8X@SX5vROsED9z$WR1cLfZpyOO!tjCKxx)hyZhoeSLR=so&T-&o z)BV4;wjyG@iu%9Hmr#rgNqqYBX&p;FF*Sj1U^r{<*fI1=LI2AcNCpJD&0B0*#OV4H z&#?6PfRTK+Gx&n@!&9H78N`s%RO#|`5J@L7Rad->dn4GIMR&*Z5w#~Y8SIP&`5AEE zWwicy7o|yWzWt`uINHOHp|JQk4jVlT7_bvT8B8rmX@(Azq|X+K)bLoHuXUJ)k(7BE zfPMcBp^fPVxZJva!=uFc<4yu6K_}dxVfnLNOCa3nVGRP$6g6*1Ec_dHe5@cb_L!b$ za3=2b0RNCWkNsqdeqak@k>EB6kh>~3oD63wRXMadk0PW(VRIDphDN0NC?OYt;aPMt zPDgARB0nGJ#LId%_5SB};U1nXu;P{RER~_OySca|KF|z3_5PNu{LM ztAn640Yc8m14~rwO7sm}GvSy+W0{^dUKQ+1A(R-jA+L~M$+!F!sH!%2i28uQdDh=9 z^np44tqpf2?^cS|%##)p*Q;Wqrc)j%l{y?s1Um^-4q5q_JAEjyaPFg4enV@$^ST{6mo z)2!&qSG`wS)%|bdyufdxL~I$-oz9XcaGW-47>^u15>2Cqkx%Ku_M}##%6Kz(jd4-! zhgngZNcOo2iKWFm%=?Xu|rp9G};U zSjo&WSK=6opy?eagX3w!I%g*}=)w7fgTc1fmuYMQ%0rXyLR;kh^mPON3%Mj;yZxKcQPt=Spo9qG8=uH`s!R@#Tj6gY3(3wnwhVCp z#y7qRP2+te*q7Cf;F+hNkxD%am7Wi!_n3rDl`{orja>3BfPxz+j321tuH%nv*WKVR zoAMU;ODb~DzYpUl`%jye%!O(&Pp@LN3->CM>Op-UsU{GHZ}}RV0BP=Goqz(71u7u_ zaHg#{_u$#aI|k^Bp=ZT?H3o7nKtX0^6JS&EdL8cKKl{>Gyw7BWbJbx$L&ba)WjuTF zJ5@mjaOu(P!9!_BGMmk)^fl-Hf&NTvY#cwKP)Zq2V+2IoJs(gmC=ij(XIJf_*%noJ zOD7(CTvh!~1A)%~vEP61eK~Nx1Ia=ONs0I*5PW`aHv^EGg(0v>U1Ls$V0;A~LJg-V zI^ithK_?ua>LCeDt{#^Vx@ime^J^9lui>nQZ{nofH_(b=6|Veg^t}1xk9;zE630}} zf%!GzZRG(f^eibTEwJ-fJ?zcHBS0;l+%iGwfc!3^`2{PB66q5VKnh-h96>E1Ua{1L zDkG)Y4YZ^9qd)qBY5`dodh*0cH8a`;cR$-6(v{zZv$SCU7F(_3rZ*{0Dq^QDh{9M$j7 zfC%4>-g@&b^^Ez_m%bdm_tv}7v(G(`lWCt-B|76-Dq(UMZa%&tqV>cvkFToXo%w87 zy3ceo_!EBQ~(~9%y}hwX-Mbm)pH#u z0`iYLzSKnS*9Zr^6a0s4(ls0sAE6+Pg;wT}_#Vdib_AXvCVW#%3da^UFi^9mETrEy zJEC!}V@p67`)QouibK~d)}?riBR|uItnQ7I;Q=Zk>%}+CcvOP8!Le?R9Q1{OUOfe_ z`*T14v%2OenlP~elMacbp(|SsaTty%v&Qvt9+)DoG~7i>Ri(y`I5S*E+CI87$;i~^ z7t}xZkD2D=-x_I|3ToH!7Bk=}jYYN^)8`0nM)0B`{ANy3Ll*YE7b*Uxqzl$?YI<9kem zJ_aYtr#|(m=rH=zs7%WDbG-FC+%}N;oyWhI^BG+)5$J^pBk(uL6i_p;nFl-mNc(8> zL%LssP9Ei`;%{_h@8Ab|`u1L96QG;AIPPI6Z7h?7BFPX{SszZLLAV6j5mLrBoDV3f z2wau%Hk>%avKrb`^sGtk8J-WQD}&G?sRkPHXSgHVH_2(mW!Glok8W%tbDU_E3g^2t>j}Y8t(iB+x!iQ|;p%g^B2+AN^?b>@&|9V$rinNxH@O zBsQf3GPf0W8Pn!MZ&boRv5CSqVNRT5;+SCv6A&8K9Zyij_&`s@VA-R_^t^j@DB5}u{MW)SpTi(Q;s{n znx7~R$+fY5l3_6Ph-JJKe`9}R#GxCo#cQ_e@n6BD+42s0S>*^2voUGJx8V_Di7<0u z39XWi6azL8YOKWV&d;=qa4s+;z>TMECD(XRJKPJY@qJ)_6H$0}x(m~L9zR%*va%A_ zxG@)b!R}#v^UT_6hoJ#Z-db%i2|29>!+7`ps&^k>(O9X7^n&WBW;$jVV0Nre)Zts; zPX|pG#7{zFx*1;w_U*^>nAT<|9*4cz?)pU@t|HuN{H^wXEBG5@AnddyF7Y4!{s;R{ z`$*3ZRL=|fH<5s4?k9` z=X;sB8O4K)H&Td*lQ3p9l&0EvmmuWuLHX?{EWNqln7u}Vb9 zA37F@tgUjyS+p@y_bT!r9@?pOeA%b2v1ojTE<%qhI!drX zJTu%kiM>5370mwOoSl`14Ga=5<7~+US>uN5iVTa(a(WqWtgY3e zMyrPMFr4#_06)8=GYn-3+`}Id8jqc$ca|P^9>g<^$_9GMSz^R$cs4g?I4ksMk#oc) z_!S8V~t}GCWEq$r`FNmx+ol zMLS5c#)(gkNrr5}e&L<6t=zsM;!7>k8|kiRZzXZ2l#UcgEIGWU}<*vi_M}|R%?jw-bB7EPSM55Q46fktJjo7&bmxi zn@Cy$t_Ex51W{~p9!#K?s?JMzJ|uMq3k8s2aQ$SatjJ)+%bwkP;dEe%6 z;296By9@C)X44QE**Apmzlr0li3mUlRyaCT;UHmF9PP|ysWe+02VwJEDJSIEh6CFmt(}UV@SLr=m)fZ z?yl@SA_5SSF)5u3&+{_Cl_?mTp~NIHz;suP{L!E%}$1R@66zswZQLBnq7_rBjB{G6G5K^uW@a%X&&S7#0!A zh&88&3XcKTHpb{QQIm-MTt%%XYY|s@HvP&v3WMLU+*H7WXN%CodjQ3^Jgafn?s?S$ z60How071vUt=2C_E3FCZW=~wN_#n132Zkw_!1;J`dg2CvZyE91c#TJZsJRCaa#j*A zofqj)L!x1z+#1i)enJ=_s4=!b?u;-jyxaMJRwXfXHQV`k*WN4w{c z9#hN9>nfS}1~~3baN7lNU)BgRskl~0RXkc4Fz9ckIsho{VFf&o3}Z{aK(wNGa#?OT zTlTR6TLqaJYXpRTo@}SUvM1I7lIzZa`%ZxC7NH7}c+)YDOtairZM@cS@9THm@n_I> zvSvq=0%|;jfmG{ecx!m|0N~FMhnge#SjhM=%?FQfL#<>*2np`&`E$`FwA#OkaxE6Z zswy6EI)GZi-8jc_9}?$h=g@`%)$wKInX2~-8JKqyuqT?09A0JunD10+hpvV2`_TD- z>L81RucJJSM1KWUGgnbD^VVB$MX$a3niBQdBjyCs>OS_;i^xoxGq`Xp`50`pyPZ!c z$;+uoH3l=Gxg{8zgvKBJT&c7^$hepAffL}MlaAw#wcbiRb)2=2ND5{DoxvRd{-b++$2498Urw!qM->Xv*uVAsVW{uj-B}^eqnzc5qdT77G;FF;-c$w7(1+N z&)9;ImD(n4d5~xb+1wxe2~Y0t63W4J+#2#(&jYbpml;Ewm_XGziH!;KBm$Ob@yKxX zkdc^3EA~jjkk#&?CrRmm9hg7+elkNK?0>MYE#-|8pC$#JXm%~}j@dvKmV*b?+oVvF z*^eXOsCx2*Cu%)p%G~F9FvalN9{$lHvQji_-ESml9aZ6ELLO+gR9VI19*zx|%@*T! zt5LD$EGJ@qlp8yY6JdgRK{I$hDXEDd?&dJrZRAE#?v4?)fs^XS zd_|~+urH_v-axVIyYIfIQ(M^*fnGEc4UO=FhaQ8}*`u-q|=l!8>PCLL@_5d_uh^acddD2 zsL9nl3YVZRY71%X$PZ!ol+T;?!WXnPz4IFu4If6qw*DsmQI#fyk}Y>nvDxs0(ekmaSs!?c~&DdGr!&#h%MO{>FeJV{%XNUDaFKM=GqtJkfPV}jqkl4 zz4rPmI;NTuU>--uefs5>!Ay?gaG8Bk1$}nJJV|B@yi+3i9vjBn{#)azu>X#u8{ltR zk4VP6ct)6;tUrN=)IVM2QqyFoZ?=XXYyGlkhaU9J&WF@Gs3Ea1fWkrULz^ZgYo=Ii z!U#mE&h}qpsi45*bhp(igR==VN=6kP7)PN4CQ>b;YsX@7q*eYBq@DpjAV%WuPk08M zD)#9*?6-HI&b|8TE8@EJ@X-@_;^~t(2Kosl4YNp>y-=*31?L3L8_o>mbZD>mW(M0! zT#wh%$rfUHBjam++H@0Y>TsOw8RwiNe<4k)bIe+5g zFGiPecKrJ|7w{bVRi42S+~>}omMy|6^Cym@w+hr$N_Ffac;ApgF<&Z{Mp!v^QIS)(vLNZ-+kp3oR<4~v;qfK6<**Is9R)t z*rVupR%MSJR9{e&pqJTHGTXawW+BH{xF;DuK=ID|AMjxm{-$)eXc9r0R1+iF*7<~Y zUjI-M+Bmn3?;b7Ht;&A2&JE^;H$2N1i;Y4)+t@z&jaiPw0nw2xi?{hN>OU6&OiE9(`kP92D)#i_=rha6tmJo=B zNT{w{v<=_|rTRC?6K4@DJ_)JEzM1c$+Wf6I-;~qnOMmuf(W|e#7Ci^&!ONfgr0hh+ z_k4V*`@xOs<10X|ZgEx$m4-m7X*X_GE>ZS0}aNK9$9IoRleoRyAQrc&GK#nZtM`nZxh? z&i^Ns>hZ^qmH+xpNF_H^|@V71TLJzRd{MWUfta1`1s2Npp?4DduJo}7vH$EvYK@pPaPa6*8LIe76d;;|$xcixXC|jt zwn=-sd@KSaBEb6x8_*=6uiYvZ{1p)0*_>TNAW+)r*eoLi@j53$X{jQN44f7h;VS-? zRLf6&^ke9&@|+sp+U{_nRxd}_g2_uFCjl|KY+h?RIvYnRd7BdW`*=Q}oF3tPKCyfv zTtF^zpcBB}Sp6UPxcxdJchV)V!(hqbwKyBm~*i6#XMD@BM3{8IV@aw9%B^TD_? z#>W?&!}!jKM&WN;WFF{gv>N;?f8{T$xR<>&*<@Fe+=FJpGi)2{>lqz?xVyvk`?DW3 z{5Op6R3Gq5oHxK<>%ZUq2QG=hn2m0}1pd$@q|+i3BN4j^jbPjDD8?p0j@s?SbZSBx zn*<~5)%@8v!^BIHhwoya1Yz9BSe@qYWJq-3ISl4bXTznx@i%@Ad3|VU4yOv^PR#ZY zI}`!Y1uwF5Nk*1u#j-};Vf>)Z7{SY{Dk)&|9vwd+WF=smXNzkHM_VT?#x!>uNuxOq z%89ed_#vzkQlG;3r=JsyDLL4DiE164$V!gz>=6;{$oefgeiZ&DhwKw$ zp7#U)Th~7h{D(7oF#FGp7c?aEro4c$XE?DIkjWy5_pZYOx~&YDAU4L6fOG;e5RY?) zgc_s?*?>d`OSdxQaCYSScw#rX%3)&5h?l?6+eYc9%p>gd3D`;en`uqT{qveRl_xRG zLFCk}4q`bNLbiRdBAm)OBbOY*^pK6?2UO8zZntVZuC{5RL?Aq9RM6U`x``b*742q zyEFVZTmRkQFDM7c$r$S&XZ&!DG&*V`CK4kC9iX7Lb0EM$HuLZZkc&bhbHB$<<2cAM z#X2YwqB3-bq~|`DMA``h{W{u!7MF6l17yp9oWtLACfFJAj3ZHPGn+2F7<4rWW-Vxg zqsP=)H{cm#vK_EZ6@UzDjEjfo@gCX-HoiEzTM$V<89igDI-5FZdhCWrG+5u`TN#vF=xBLI=H3q_sEDgtF*EWDry9cXR%jPNCrBWwXdIxqJwJ$wBt7@%1a45zpuOfQ&a5 z(M?=(U5H?NxpDnreB(HwB+a`vHylgUgbk444Ed+1H>~~ zcS(|i52IVGPA;Oi(ZeG^xO`!!V|A+74jj>f*LE@zGeCJhyzf^4gu_FPdY$-;dwB)K z@KZyY0DIqbWni!CDU;Ip&l|2nbEsRpF3Y z;gwjQ`XJ23ANheEKiI<#$-uOY!*65#Lof5j^^X$bX7InC@wt=SUXuJM@{9}rg{7O( zE3bT4=S=Vn!S8(gJIdi@){Q-I$i*Ml{%>df)CAaNoy=mu_WSZ@*gOeaIA6MOSS_I< zWrY3SiIQ&rd@KT_7sQM|Qx14j@TLt%H3_Dv$wOmA8U~PxmIx`o&~MS^Np(oxT>WvI zk6tI=o1p6XHzPuCh`Qz>vhl(DOnv5SxTX&h{BK=9i*B1OCdyks zSRO&Zv_cj_TP)W}*>qA{)qU*$gMh#00K&7HLW4UUGwREWX1oZ**Ws@ila@9T;aE4J zzefg@z~v%>LOSur^C>MrY3&CTK=zlsmBM#Z> x3evmWr$%ii4IO!0!WqzQLjuj% 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 literal 0 HcmV?d00001 diff --git a/icons/multisrc/hotnovelpub/icons/thnovels.png b/icons/multisrc/hotnovelpub/icons/thnovels.png new file mode 100644 index 0000000000000000000000000000000000000000..a18ad415f03f20a868bd1047e519b0bf8facd8d5 GIT binary patch literal 5780 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 literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/bacalightnovel.png b/icons/multisrc/lightnovelwp/bacalightnovel.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ee2c10fb522c8ba3542d3db8de0dcd27c372d2 GIT binary patch literal 3793 zcmZu!2T)U8(+*9VV5kBjQUXW`gd!yvkO)X9fdDT(Ql$415b4r{m!edqHw_qyfQU#H zk=_KP_udReR1EdM%s2DTd^6wNId|`zIeT{R?z7LH6Q_@Uz(B`E2Lgc@P?~B6fV93G zG*^K4@~g)RfZ%m{jAdnvn1lk4`!(V|w*F96LIy8W_S8TN( zsDUmnpMvI+RA7eIOVi931fplT9Au!Z91dWT8i&$Vr`{x|yDmV@#WfKOSkFPJsTu`L z6LX1%h7&DaDnWZuFQV@8KBeI|0q?(!5F9fXH?NpK4t5A!uQRc5gwJ!94F0l#ANgH8 zQ_8I#MJt6E&Kx6WG9T?-9i2AJIrjQ`tj46SfKLl&*5#!Ir{w6z|w7++nqkfC>V_$IHB7=i5C^A!uTr(vKf(W6IO&z9v#TXqOU2gF) zsk&MUoui#fDD5@f$6%OB^z!kqUsJxD=xgxX0V&`%!NPiZkW3w}+}SRES1KjWm1;!GB?e`mxPHM>RfcS{dSI$eH z!qCVFHxW`?Tx{7InhQjvIQ3M`E*u=Gig|5qt?AFSD}2sjmw6q&h*Ky?nU|f_)J#WPM}I?6K&ffs5Bh+ z3OPi>At-TdLt3uVWBi9IR`s@>-v(Az&!=+c@{I3aCf;qPE)kE%_i%bNbPo+}WIE;* zR#koL7FT9%2a-n%Kqdof;RVli4r?vC-4Fb&arg_c6WF0bWcxDuXY|4A`l2Q zbwZmB><|<)kdNZRLe-a^`1U31qun_LZboI&DvN|={R_W&8LaC)4-bjN#6WsZGV%fv|MWe)9DjSX^A>t*EF7K0WBFt*bkK zLRB?G{604J#=YZEwUp5N?Hd?@KyWMiMD4X+gcvroY!>8|IuHcm;0 zIDh|<(b4w({VJZRAbuQ60f8V|A(l!YjLgjR4HrKkt-Z9W5Bx2vod4V!7K?Wdv=f7F z@`v}>*`A-0RyQ_E)-1m;zS5a?Zf*%nOE$G{dtWtA_4o+KtEF-Ei%6e0n22x`);E}`j2wZ$&J zBVx{@Yt09P!KeaIdTZq3^BuSST?9a>hO29t4f1xvE!};S#Y`Q}9w6gGd5D0!JaaM% zZC`~{)G<|4ZmDH83=9lF^)UgiQj!Pyt&=NI;x^@&`Wn<>W}Oc2F(xJt?Ck}$GsJB! z&d<12LM#$t0N8Xp^C zFv;FQ4KxMLT-ByJ*9@K8mb~`2%ijGQ(U!EoTd65e#-FS?uK4wqnzOTr#Yc~UfT#~1 zXw^-5G6ise_Rv~0)Up6$5S+1%CkPA0U)sNrDPY;-_ebu zA{&IoVr4uGKLGHUsD3kJ23l}@hl20=Rpop=$y8@m z3@bpa{qM*?&CSg@${|^rkfM?)VdSVRbDUadh5?R^mz&XdwVP>rdis~_i5kG900;;K zht;zNx))k+>H`k=p-`yI`eL(}f(|dZd(u2(_hAZvm6o$V7C^U5{;kcDgyX5%0Uaud(O+s)+nZH*~n#!h=~=~*8bu-D(T zEZj*}%f#e51qB5p6}5a&zck#Ng)h8KXK!!sw$PFbwWynALGF1em;wq`p`^8bHr{lt zVSr?Ag`JXl>A{G_uwfIO;CarJ^ZM?ZgrSY%*RP}QxQ@^IU0qj${AuOk&X&NHk!?9S z^gGC)9r-*y(l^c}O}S~%sETvj{OW|(M$sK@^$T{=`$FFMh$$nNlm6UI*YaNLWdk~7 zX<1o2(SPB0`RG>vkM1a-=-l)G#w_JumV5W=4<2ZV1a;h=Y3Cz67n%@={^km^WVPPsjhfM3wy#ENJ zc#+pDtWp_~;~Pd6rG^epyI^C!n6_qPW!1N`(o zp~jE)^ppf(>x2a7nG8(la9RGF@FHF0;(7t4rVHJa+ZW@_uROZ^d^XaVK<94y*(+9V z)f(E%QWqE$B*U)%;tn~$I>r^|>VAH|?PiqO9{V={#}GY8DLPA}ULls|nK0LhNIuH} zCA~{oK(UA5nGd--rn{%dX1GYb9NU=j^XJc?JZ%W`DS&0pY$nHOki^S)ptIL_E-0_} zXC8J7aCukXs0^X9bqd~THyZc=Dx_sk8+;RZv=d8ZpGs`#u>G`0V$03VJ>74=#(8MP zA#r&;uxiNxHZd?XJT%S4j<`4)HGIV8OSjiV40t@q>qI32aN$<)3eOKaS9~e`7W9CA z*wVtIj=?wYzj5XwHS>usd-DX+B~wNw(}ZjcNsUY(I9>Qr%r)XHacY~Jn+0#)M9Vl1 zdhLu?By22rky!`7_THUUJU%(`{$zQLJ7zu(Ob$)A{q!dGV!Zd2XA6RojhB`TNr6Ik zOLsFHBxNS;qLoScVTiOtDiEf;d!KetgNyLc}&Hm6nRM~i@o}Ix5!QNa#@w_A@=SCA+KXIw|!>CFILdh?W zj_kN7@|QaGtpGhOC(HO4U@d3vY$GgK=+x2i@`}Rt+1jqN%Qn;dsQ)Lg5yxH>)jpz% z)+?DIa-}3c479OyF#%@#On;g^EONq9ApP(!LPOY&eenXM7`(2VAItg$xVj1A)J<`A zHaL6uBOiM}fTUs4vQU^bR0?J!Ed!Ui1Bc0q!C-J040nyw@4pn>J?)$wo?I$OLt%$jy;*Th-)e4#JT_Be)zoq0iRp`gfkjy2h{)o(7@w7i91UE2T<@1 zzq^}paz{XE#53+dx%^d!4K-2QK~z6PYRBd1^|0|{{iUP~`cG`>#T)UM1Tf=hWho3O5KMg~UoEvwxbItPm?CN->{C<6rkDQ^f zQ&x5cN0@4I)2h$em3Z&cN)>eaE~83H2pTYTx2f)#UG3#dh>74SHN{c9)+OHJAYtF& zAj6AV5N5h2%Wl*P-R@|hc@FywpK@dENx4XEbuo8Jb`soi3*u#n>6{il?PP$AqIO8_ zos!Ma5a4m~tLnTDh%bPMMm>A&HD&}qcYJbyyz3|m=jD^v*?8Bx?oy|#Fz zsN6%M2W~vtQEKB6PK$)^W8B_kH7CYfe*K)()~SKschNpu3PF47N!?dBiOJXCx~ z&5?Qwx3zSAVIl|N{7TLLFBe-6ev<3iCf(O(-bRG`JW!GQDD3f!DJT@6r%zZ=hTul_ zm@F_gv)0SF#KQg7jn8FTXhhUEaOVM+v!m`)r!R(<-yp5W3_Zvb+21h%GbMxsdrmpj zA;$z<5ts?oK$^~{&M*y`3*WX=7N&WRcAP*UC0;5YS0S??B7IFJ+7O6w0s1om)<-~p z8f4S|?+rD>fE1}KW$7+U$UOYj2_n&~#L?D#6_l{x0PiyG!P!i#0Ne9m?|D-S=; zadFYxm`n^^b&7g>WnwM#Dt2s~KR-ocA9On~*c4lvkA%>UDYf9iXvb^wFaa{kJUZYw zx6->?;r4sI5x~`WO&}_1GHs@TGUNSce@aZLZK9)hK?Z|U`g1Ai7Pc7Q!=eadI|`~S zUq53LqjVG%YVYdE{^iTg_ZS4f9)pBFQaPY0uFG?mvS_BWYn?RQvf1~UM4~m97}L;a z3(?~CBAW6Skvw&eLT;0<=I>`<_`7bUs3Tms!i{M$qrMQ;k)NpJ$zYU+IXc1R2T17} zqo(<0fE$LqTW6!gu=wW*`TveS21G9uW2-58j-R{k*l{oJMuP#XLU>i4icMdZ-vqro zr+!~D;860cdZ#JL3}d&2VjqE+F|l6!8bU5?2N%Fb)dHuzB2 z4_BqHC#4>4zgNX4u_y6ybGtg#mhqY;qLGwxfgii2x>@I!rEB_@KQ^!Z`J`$IgX~*( zwY>haty{9C%6^JVIBpd*Wa#n5!j`g^*A#E|u5UG^km73;|Khy=A2ty3f!n_Kn-GpG zk`^H%lX|m3Rgl<6xn!M~ZapnKUkJk_=rS!A@oMRF3z6AW1xS_@k6M5l^gfMG3S~W1 zf0CQ=vKO>*+ZkrQfAr5Kua-XSk;6zdq{4DD5?5fE{&Ku|^x)STM!^2Kff2o^x^L2S z3uXGgDZH1jIGZN9pdJ%NyZnL+ZdU4ffBDY9V)`Gl?0x%{OV8z&9sxiN2rDayJ-v>S z9NsYG!0eQ*rz*1RI|-F@godRF^n0WFSL5r(B(|1OVOdN3j6^t)1+uvk2W`VC4)Nz6 z+pd#gU8wf8LBt|vbLk^HIAq=_+Gd@6b`&M3kJK~CkPDx$uq3kU0!fE)PK$U~=1if? zquat9-xIqP6Zp%8;ujEY)C8@q$Djyyy>G~SzE+s9KGe)HdV_cqfakS?!nM=Dvv5|w zShMW<&B~6G!9t2wC()cMP3@9mAIC-V&Gq*x9Js&Qj%9Mh7I|xYy{CWmNJKXZG&cM_ ztiIR81%;RGe|{ny>7jAHKe;O7gtkZrGuAvU4xKcPi<@P%MocE<{yG`Bb1MMei{RPl HPR{uoR$U8B literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/daotranslate.png b/icons/multisrc/lightnovelwp/daotranslate.png new file mode 100644 index 0000000000000000000000000000000000000000..989b25f911d51b998d1bd9647fd4aa532cf3f695 GIT binary patch literal 7328 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 literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/knoxt.png b/icons/multisrc/lightnovelwp/knoxt.png new file mode 100644 index 0000000000000000000000000000000000000000..a419e82815f2d29e893e4c4222fe63e9ace0da90 GIT binary patch literal 8420 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 literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/lightnovelbrasil.png b/icons/multisrc/lightnovelwp/lightnovelbrasil.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf89553f029e9e2cb3491c75f208e673b805732 GIT binary patch literal 5009 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 b67By1;?kH{ad0BD00000NkvXXu0mjfDDsR$ literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/lightnovelfr.png b/icons/multisrc/lightnovelwp/lightnovelfr.png new file mode 100644 index 0000000000000000000000000000000000000000..045111fcbc6b521c8b84e7c510f8ca90539a9467 GIT binary patch literal 7333 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 YNX!4HiScQ<r7l7pm_ASpQ@-O?i6NQZ>f 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 RF~^<(H6<;@DtXJW{{x-=h@$`i literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/novelsparadise.png b/icons/multisrc/lightnovelwp/novelsparadise.png new file mode 100644 index 0000000000000000000000000000000000000000..55deeb83d166b3941129aa5bfb1e38f2f8d7e324 GIT binary patch literal 12543 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 VdHhO+;mH61002ovPDHLkV1gDf^kV=3 literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/systemtranslation.png b/icons/multisrc/lightnovelwp/systemtranslation.png new file mode 100644 index 0000000000000000000000000000000000000000..fb15d4782b44eeaa698160fea7563beffdd4a3af GIT binary patch literal 8933 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 Cu1Yxo literal 0 HcmV?d00001 diff --git a/icons/multisrc/lightnovelwp/whitemoonlightnovels.png b/icons/multisrc/lightnovelwp/whitemoonlightnovels.png new file mode 100644 index 0000000000000000000000000000000000000000..ee12995283b6920601ba0930324742f397c16654 GIT binary patch literal 2625 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 literal 0 HcmV?d00001 diff --git a/icons/src/en/genesistls/icon.png b/icons/src/en/genesistls/icon.png deleted file mode 100644 index 56068c59d312719176f4230a406e81184e34e287..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14690 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 diff --git a/icons/src/en/noblemtl/icon.png b/icons/src/en/noblemtl/icon.png deleted file mode 100644 index 39423fbcdaa793211d677998f4aea8a9a21df3b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12407 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 diff --git a/icons/src/en/novelbuddy/icon.png b/icons/src/en/novelbuddy/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b314f766226ccd3f2148112975e19e71507f22 GIT binary patch literal 3198 zcmdT`i8s~X7XMx@qL2nfGKSwz!_z?KDPxFmN#-e%F(HJD6zLW+&n5GeDVfQTp-h)@ zGjvT|%2Z}ucfZ@O_xttUpYYE5>~r=x`?L31>#WaOXNT)vQe$S~WI_;xSwme}kGfj^ zv|Y5+c}VsC&j89pPfZC$=sXcf-7vbT8+#xKyWmelY3LnWMi81-T`dC@Y7m0~Dhv!L z`uaaXb^gIWAqG1U{EG)qN z`#>Z@Z7uZmL3=x7WxtHFuZ*W zQ&SKe3@t72?i~~tQ?>CQ)%~~gw_1M7FIA(jhpOZGTY8uD5%g|;4e!E%FPl41;HAOO zDyZTT-!!(gZTz5mcy4u@La2!_Zu--trXt#M(@Qfd{aB$rK8?3z z(9Noh;f-L)H|bRh)GjrL?$=R1D*YbScH1&`IuEy(W>#+zMe>B}dMstJ=$ntU32 zITnXq7#Sa{X(M!gOJzPR{bpkvh%HB@y{)`QSY8~dy+L{;>$l-Q?Qq|xWp1bOPTi+_ z?dgi^JQA;ZJ5ejyD-1k=!NLAv%X2~XJ?jZoFYkoi1J&0Q+4O2lahI{6X#cPmQ$$k2 zzZyov9}OsJG_Ny z5V0^eu6>6&*|{e!whExJN{=2skmKHeghZ?G@vTG&cKLE}V%WU+02scLvssbr2v@AQmXX^4Zeo}$&|i|!hWgn?I>FE92f z5UbOrqx1|<#cCXX-NRs^UQoCX(|u5^((}+(lor>cF53@k=g)}27kl9gfMM%JC_4P&UjVy&{Lls9BKULNwsGm))-EaGlNiKlA{^b>=)_>_UT#{glT4G{E@UI?krdYifP+h+O<$P}0ew zdhbz9>&bVAX4X*jv9vgQA!4OMiAZOzeUWIRIo4O#F1IMCacy8cGhQb5CH;xr!)9!8 z^K+Vt%o(%{p*H0dMRA7NTb#__i!N)~tBE>|cJ>jCzOHpx6ynum3FhM5*%^jnQnF^J zRO0S|#)-BQ2D zT_?p}$w6{}bt-R8+?+LuryPYni1E?3dVucr8$OJ@QCTHERN)9?X>!t5-MxHbpEBc% zLf~9hL!T?zFPi2^f7^*(bkXfS?G^Pvw!QD{!!U$C8jfxmOJ4`$;B|4p$q7SFvhS;nLdum9dDmUK#SIu8AIp zrm$!_s?6okyYd8srn9%yQ8|?}K8$M`D||;=`cCOHh}>%_dQcor_axga)2psjCq@05 zJYC0KTxy`r*}TOde#sR2+W36VYux51PUk)yvXBD+&ig8 z%C_TwN}FqTMpb_F?rFx9r}&DKYb>MrPaG6OD@+Hk7$3gAC5pbUNq$+tV%+ZO*pirO z&F<~he?SsjnV5Iz3R>M}I3ZKNds+mOOk<#h8+}LWeNrgV(~Nq!N)de3mC-PwYZSZG zZu49|Z|DMtBcXMSx2`k?gIOyt0)VTB+j7jc}q(9uIU&v$ZecAEo*VcHY=Z#9n! zh=ktBe^#}2r5~q$=HSv4%ubFD#<(4G%8o=!GwQyQ+=(#Ot9Hy)GT?e2sr#5bQ4l7H z?V3IyS10#8I_!Iy^i~!{wl4#{yc$fPjQDs{*=GzxyD61F1f#@6$(>YnQG-e~ti-?3O ziFNadCf>!=tPN2=msRhGhXOwm;?caPjk5HN2xpCv!`j9o73^Cf=DKzCb%?@RzWaIh zi0^iD>jJYD8_xo)aNDu9-S&REe&?2VSmkRh=TkShHxFdlv&!#~BV^l~uYYQ$&6OG5 zq~rurwv%bwd%k7*`)xGo=r~>H3h@2j(%VS+>Mf0*N(>D2>4>Xra*J4qhsqB!LA6ye7jwY@?#R4yqOD_I8r8(~ZHr~m)} literal 0 HcmV?d00001 diff --git a/icons/src/en/novelfullme/icon.png b/icons/src/en/novelfullme/icon.png deleted file mode 100644 index 952e1ae6ea1237cadc6e23b043b344678f9b6eab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1540 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 literal 0 HcmV?d00001 diff --git a/icons/src/en/readlitenovel/icon.png b/icons/src/en/readlitenovel/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..165a2e518cac41ff811386cde1d20235e2fc3ea3 GIT binary patch literal 7185 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 literal 0 HcmV?d00001 diff --git a/icons/src/en/wtrlab/icon.png b/icons/src/en/wtrlab/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..169abdba5304da9bb65b5ba13e2e1c8fe378e41c GIT binary patch literal 3680 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

h=Wrsk5ZZ{qmW#ygGLefUc&f@wE}w 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 Uujt&V!Z literal 0 HcmV?d00001 diff --git a/index.ts b/index.ts index a5c37f0..0e49ffd 100644 --- a/index.ts +++ b/index.ts @@ -88,22 +88,44 @@ app.post("/searchNovels/", async (req, res) => { res.json({ error: String(err) }); } }); -app.post("/parseNovelAndChapters/", async (req, res) => { +app.post("/parseNovel/", async (req, res) => { try { - const sourceNovel = await pluginApi.parseNovelAndChapters( + const sourceNovel = await pluginApi.parseNovel( req.body["pluginRequirePath"], - req.body["novelUrl"] + req.body["novelPath"] ); res.json(sourceNovel); } catch (err: unknown) { res.json({ error: String(err) }); } }); +app.post("/hasParsePage", async (req, res) => { + try { + const parsePage = await pluginApi.hasParsePage( + req.body["pluginRequirePath"] + ) + res.json({ hasParsePage: parsePage !== undefined }); + } catch (err: unknown) { + res.json({ hasParsePage: false }); + } +}); +app.post("/parsePage/", async (req, res) => { + try { + const sourcePage = await pluginApi.parsePage( + req.body["pluginRequirePath"], + req.body["novelPath"], + req.body["page"] + ); + res.json(sourcePage); + } catch (err: unknown) { + res.json({ error: String(err) }); + } +}); app.post("/parseChapter/", async (req, res) => { try { const chapterText = await pluginApi.parseChapter( req.body["pluginRequirePath"], - req.body["chapterUrl"] + req.body["chapterPath"] ); res.send(chapterText); } catch (err: unknown) { diff --git a/package-lock.json b/package-lock.json index 818085d..1dd3b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,22 +11,23 @@ "dependencies": { "body-parser": "^1.20.2", "browserify": "^17.0.0", - "cheerio": "^1.0.0-rc.10", + "cheerio": "^1.0.0-rc.12", "cli-color": "^2.0.3", - "dayjs": "^1.11.4", + "dayjs": "^1.11.10", "express": "^4.18.2", "less": "^4.2.0", - "module-alias": "^2.2.2", + "module-alias": "^2.2.3", "sanitize-html": "^2.11.0", - "ts-node": "^10.9.1", - "tsc-alias": "^1.8.6", - "typescript": "^5.1.3", + "terser": "^5.27.2", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", + "typescript": "^5.3.3", "urlencode": "^2.0.0" }, "devDependencies": { - "@types/express": "^4.17.17", - "@types/jquery": "^3.5.28", - "@types/node": "^20.3.1", + "@types/express": "^4.17.21", + "@types/jquery": "^3.5.29", + "@types/node": "^20.11.19", "pre-commit": "^1.2.2", "tsc-watch": "^6.0.4" } @@ -42,6 +43,19 @@ "node": ">=12" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -50,6 +64,23 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -136,9 +167,9 @@ } }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -160,9 +191,9 @@ } }, "node_modules/@types/jquery": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.28.tgz", - "integrity": "sha512-2o/vlzaDXiGWFrHz/PhX88cy68UDc8NBBT9i1nU+EtRkKgRxMWLTlUkEJb+MRfhIw6wKK8RDmfoEXClH2PsIyA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", + "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", "dev": true, "dependencies": { "@types/sizzle": "*" @@ -175,9 +206,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/qs": { "version": "6.9.7", @@ -981,9 +1015,9 @@ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" }, "node_modules/dayjs": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz", - "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==" + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { "version": "3.2.7", @@ -2339,9 +2373,9 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/module-alias": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", - "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" }, "node_modules/module-deps": { "version": "6.2.3", @@ -3231,6 +3265,23 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spawn-sync": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", @@ -3384,6 +3435,28 @@ "acorn-node": "^1.2.0" } }, + "node_modules/terser": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.2.tgz", + "integrity": "sha512-sHXmLSkImesJ4p5apTeT63DsV4Obe1s37qT8qvwHRmVxKTBH7Rv9Wr26VcAMmLbmk9UliiwK8z+657NyJHHy/w==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -3438,9 +3511,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -3480,9 +3553,9 @@ } }, "node_modules/tsc-alias": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.6.tgz", - "integrity": "sha512-vq+i6VpE83IeMsSJVcFN03ZBofADhr8/gIJXjxpbnTRfN/MFXy0+SBaKG2o7p95QqXBGkeG98HYz3IkOOveFbg==", + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.8.tgz", + "integrity": "sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==", "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", @@ -3599,9 +3672,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3633,6 +3706,11 @@ "undeclared-identifiers": "bin.js" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3793,11 +3871,35 @@ "@jridgewell/trace-mapping": "0.3.9" } }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -3875,9 +3977,9 @@ } }, "@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "requires": { "@types/body-parser": "*", @@ -3899,9 +4001,9 @@ } }, "@types/jquery": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.28.tgz", - "integrity": "sha512-2o/vlzaDXiGWFrHz/PhX88cy68UDc8NBBT9i1nU+EtRkKgRxMWLTlUkEJb+MRfhIw6wKK8RDmfoEXClH2PsIyA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", + "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", "dev": true, "requires": { "@types/sizzle": "*" @@ -3914,9 +4016,12 @@ "dev": true }, "@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/qs": { "version": "6.9.7", @@ -4601,9 +4706,9 @@ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" }, "dayjs": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz", - "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==" + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "debug": { "version": "3.2.7", @@ -5656,9 +5761,9 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "module-alias": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", - "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" }, "module-deps": { "version": "6.2.3", @@ -6311,6 +6416,22 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "spawn-sync": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", @@ -6448,6 +6569,24 @@ "acorn-node": "^1.2.0" } }, + "terser": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.2.tgz", + "integrity": "sha512-sHXmLSkImesJ4p5apTeT63DsV4Obe1s37qT8qvwHRmVxKTBH7Rv9Wr26VcAMmLbmk9UliiwK8z+657NyJHHy/w==", + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -6493,9 +6632,9 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -6513,9 +6652,9 @@ } }, "tsc-alias": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.6.tgz", - "integrity": "sha512-vq+i6VpE83IeMsSJVcFN03ZBofADhr8/gIJXjxpbnTRfN/MFXy0+SBaKG2o7p95QqXBGkeG98HYz3IkOOveFbg==", + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.8.tgz", + "integrity": "sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==", "requires": { "chokidar": "^3.5.3", "commander": "^9.0.0", @@ -6604,9 +6743,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==" + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==" }, "umd": { "version": "3.0.3", @@ -6625,6 +6764,11 @@ "xtend": "^4.0.1" } }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 4cd4a60..11e7176 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,17 @@ "dependencies": { "body-parser": "^1.20.2", "browserify": "^17.0.0", - "cheerio": "^1.0.0-rc.10", + "cheerio": "^1.0.0-rc.12", "cli-color": "^2.0.3", - "dayjs": "^1.11.4", + "dayjs": "^1.11.10", "express": "^4.18.2", "less": "^4.2.0", - "module-alias": "^2.2.2", + "module-alias": "^2.2.3", "sanitize-html": "^2.11.0", - "ts-node": "^10.9.1", - "tsc-alias": "^1.8.6", - "typescript": "^5.1.3", + "terser": "^5.27.2", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", + "typescript": "^5.3.3", "urlencode": "^2.0.0" }, "_moduleAliases": { @@ -38,9 +39,9 @@ "@typings": "./.js/types" }, "devDependencies": { - "@types/express": "^4.17.17", - "@types/jquery": "^3.5.28", - "@types/node": "^20.3.1", + "@types/express": "^4.17.21", + "@types/jquery": "^3.5.29", + "@types/node": "^20.11.19", "pre-commit": "^1.2.2", "tsc-watch": "^6.0.4" } diff --git a/plugins/arabic/.gitkeep b/plugins/arabic/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/arabic/kolnovel.ts b/plugins/arabic/kolnovel.ts deleted file mode 100644 index b04461b..0000000 --- a/plugins/arabic/kolnovel.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; - -class KolNovel implements Plugin.PluginBase { - id = "kolnovel"; - name = "KolNovel"; - icon = "multisrc/wpmangastream/icons/kolnovel.png"; - site = "https://kolnovel.lol/"; - version = "1.0.0"; - - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}series/?page=${pageNo}`; - - if (filters.genres.value.length) - link += filters.genres.value.map((i) => `&genre[]=${i}`).join(""); - if (filters.type.value.length) - link += filters.type.value.map((i) => `&lang[]=${i}`).join(""); - - link += "&status=" + (filters?.status ? filters.status : ""); - - link += "&order=" + (filters?.order ? filters.order : "popular"); - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.maindet").each(function () { - const novelName = loadedCheerio(this).find("h2").text(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novelUrl = loadedCheerio(this).find("h2 a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetch(url, { headers }); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio("h1.entry-title").text(); - - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - - loadedCheerio("div.serl:nth-child(3) > span").each(function () { - const detailName = loadedCheerio(this).text().trim(); - const detail = loadedCheerio(this).next().text().trim(); - - switch (detailName) { - case "الكاتب": - case "Author": - novel.author = detail; - break; - } - }); - - novel.status = loadedCheerio("div.sertostat > span").attr("class"); - - novel.genres = loadedCheerio(".sertogenre") - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - novel.summary = loadedCheerio(".sersys") - .find("br") - .replaceWith("\n") - .end() - .text(); - - let chapter: Plugin.ChapterItem[] = []; - - loadedCheerio(".eplister") - .find("li") - .each(function () { - const chapterName = - loadedCheerio(this).find(".epl-num").text() + - " - " + - loadedCheerio(this).find(".epl-title").text(); - - const releaseDate = loadedCheerio(this) - .find(".epl-date") - .text() - .trim(); - - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetch(chapterUrl, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - loadedCheerio(".epcontent > div, i").remove(); - let ignore = loadedCheerio("article > style").text().trim().split(","); - ignore.push(...(ignore.pop()?.match(/^\.\w+/) || [])); - ignore.map((tag) => loadedCheerio(`p${tag}`).remove()); - - let chapterText = loadedCheerio(".epcontent").html() || ""; - - return chapterText; - } - async searchNovels( - searchTerm: string, - pageNo?: number | undefined - ): Promise { - const url = `${this.site}?s=${searchTerm}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.maindet").each(function () { - const novelName = loadedCheerio(this).find("h2").text(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novelUrl = loadedCheerio(this).find("h2 a").attr("href"); - - if (!novelUrl) return; - - novels.push({ - name: novelName, - url: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - filters = { - order: { - label: "ترتيب حسب", - options: [ - { label: "الإعداد الأولي", value: "" }, - - { label: "A-Z", value: "title" }, - - { label: "Z-A", value: "titlereverse" }, - - { label: "أخر التحديثات", value: "update" }, - - { label: "أخر ما تم إضافته", value: "latest" }, - - { label: "الرائجة", value: "popular" }, - ], - value: "", - type: FilterTypes.Picker, - }, - status: { - value: "", - label: "الحالة", - options: [ - { label: "All", value: "" }, - - { label: "Ongoing", value: "ongoing" }, - - { label: "Hiatus", value: "hiatus" }, - - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: [], - label: "النوع", - options: [ - { label: "إنجليزية", value: "english" }, - - { label: "روايةلايت", value: "light-novel" }, - - { label: "روايةويب", value: "web-novel" }, - - { label: "صينية", value: "chinese" }, - - { label: "عربية", value: "arabic" }, - - { label: "كورية", value: "korean" }, - - { label: "يابانية", value: "japanese" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genres: { - value: [], - label: "تصنيف", - options: [ - { label: "Wuxia", value: "wuxia" }, - - { label: "Xianxia", value: "xianxia" }, - - { label: "XUANHUAN", value: "xuanhuan" }, - - { label: "أكشن", value: "action" }, - - { label: "إثارة", value: "excitement" }, - - { label: "إنتقالالىعالمأخر", value: "isekai" }, - - { label: "إيتشي", value: "etchi" }, - - { label: "الخيالالعلمي", value: "sci-fi" }, - - { label: "بوليسي", value: "policy" }, - - { label: "تاريخي", value: "historical" }, - - { label: "تحقيقات", value: "%d8%aa%d8%ad%d9%82%d9%8a%d9%82" }, - - { label: "تقمصشخصيات", value: "rpg" }, - - { label: "جريمة", value: "crime" }, - - { label: "جوسى", value: "josei" }, - - { label: "حريم", value: "harem" }, - - { label: "حياةمدرسية", value: "school-life" }, - - { label: "خيالي(فانتازيا)", value: "fantasy" }, - - { label: "دراما", value: "drama" }, - - { label: "رعب", value: "horror" }, - - { label: "رومانسي", value: "romantic" }, - - { label: "سحر", value: "magic" }, - - { label: "سينن", value: "senen" }, - - { label: "شريحةمنالحياة", value: "slice-of-life" }, - - { label: "شوجو", value: "shojo" }, - - { label: "شونين", value: "shonen" }, - - { label: "طبي", value: "medical" }, - - { label: "ظواهرخارقةللطبيعة", value: "supernatural" }, - - { label: "غموض", value: "mysteries" }, - - { label: "فنونالقتال", value: "martial-arts" }, - - { label: "قوىخارقة", value: "superpower" }, - - { label: "كوميدي", value: "comedy" }, - - { label: "مأساوي", value: "tragedy" }, - - { label: "مابعدالكارثة", value: "after-the-disaster" }, - - { label: "مغامرة", value: "adventure" }, - - { label: "ميكا", value: "mechanical" }, - - { label: "ناضج", value: "mature" }, - - { label: "نفسي", value: "psychological" }, - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; -} - -export default new KolNovel(); diff --git a/plugins/chinese/69shu.ts b/plugins/chinese/69shu.broken.ts similarity index 100% rename from plugins/chinese/69shu.ts rename to plugins/chinese/69shu.broken.ts diff --git a/plugins/chinese/69xinshu.ts b/plugins/chinese/69xinshu.broken.ts similarity index 100% rename from plugins/chinese/69xinshu.ts rename to plugins/chinese/69xinshu.broken.ts diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.broken.ts similarity index 100% rename from plugins/chinese/linovelib.ts rename to plugins/chinese/linovelib.broken.ts diff --git a/plugins/english/LightNovelReader.broken.ts b/plugins/english/LightNovelReader.broken.ts deleted file mode 100644 index 8783eda..0000000 --- a/plugins/english/LightNovelReader.broken.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Chapter, Novel, Plugin } from "@typings/plugin"; - -export const id = "LNR.org"; -export const name = "LightNovelReader"; -export const icon = "src/en/lightnovelreader/icon.png"; -export const version = "1.0.0"; -export const site = "https://lightnovelreader.org"; - - -const baseUrl = site; - -export const popularNovels: Plugin.popularNovels = async function (page) { - const url = baseUrl + "/ranking/top-rated/" + page; - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Novel.Item[] = []; - - loadedCheerio(".category-items.ranking-category.cm-list > ul > li").each( - function () { - let novelUrl = loadedCheerio(this).find("a").attr("href"); - - if (novelUrl && !isUrlAbsolute(novelUrl)) { - novelUrl = baseUrl + novelUrl; - } - - if (novelUrl) { - const novelName = loadedCheerio(this) - .find(".category-name a") - .text() - .trim(); - let novelCover = loadedCheerio(this) - .find(".category-img img") - .attr("src"); - - if (novelCover && !isUrlAbsolute(novelCover)) { - novelCover = baseUrl + novelCover; - } - - const novel = { - url: novelUrl, - name: novelName, - cover: novelCover, - }; - - novels.push(novel); - } - } - ); - - return novels; -}; - -export const parseNovelAndChapters: Plugin.parseNovelAndChapters = - async function (novelUrl) { - const url = novelUrl; - const result = await fetchApi(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Novel.instance = { - url: novelUrl, - chapters: [], - }; - - novel.name = loadedCheerio(".section-header-title > h2").text(); - - let novelCover = loadedCheerio(".novels-detail img").attr("src"); - - novel.cover = novelCover - ? isUrlAbsolute(novelCover) - ? novelCover - : baseUrl + novelCover - : undefined; - - novel.summary = loadedCheerio( - "div.container > div > div.col-12.col-xl-9 > div > div:nth-child(5) > div" - ) - .text() - .trim(); - - novel.author = loadedCheerio( - "div.novels-detail-right > ul > li:nth-child(6) > .novels-detail-right-in-right > a" - ) - .text() - .trim(); - - novel.genres = loadedCheerio( - "body > section:nth-child(4) > div > div > div.col-12.col-xl-9 > div > div:nth-child(2) > div > div.novels-detail-right > ul > li:nth-child(3) > div.novels-detail-right-in-right" - ) - .text() - .trim(); - - novel.status = loadedCheerio( - "div.novels-detail-right > ul > li:nth-child(2) > .novels-detail-right-in-right" - ) - .text() - .trim(); - - loadedCheerio(".cm-tabs-content > ul > li").each(function () { - let chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (chapterUrl && !isUrlAbsolute(chapterUrl)) { - chapterUrl = baseUrl + chapterUrl; - } - - if (chapterUrl) { - const chapterName = loadedCheerio(this).find("a").text().trim(); - const releaseDate = null; - - const chapter: Chapter.Item = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; - - novel.chapters?.push(chapter); - } - }); - - return novel; - }; - -export const parseChapter: Plugin.parseChapter = async function (chapterUrl) { - const url = chapterUrl; - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#chapterText").html() || ""; - - return chapterText; -}; - -export const searchNovels: Plugin.searchNovels = async function (searchTerm) { - const url = - baseUrl + `/search/autocomplete?dataType=json&query=${searchTerm}`; - const result = await fetchApi(url); - const body = await result.json() as { results: ResultsEntity[] }; - - const novels: Novel.Item[] = []; - - body.results.forEach((item) => - novels.push({ - url: item.link, - name: item.original_title, - cover: item.image, - }) - ); - - return novels; -}; - -export const fetchImage: Plugin.fetchImage = async function (url) { - return await fetchFile(url, {}); -}; - -interface ResultsEntity { - overview: string; - original_title: string; - link: string; - image: string; -} diff --git a/plugins/english/NovelFullMe.ts b/plugins/english/NovelFullMe.ts deleted file mode 100644 index bca361a..0000000 --- a/plugins/english/NovelFullMe.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; - -class NovelFull implements Plugin.PluginBase { - id = "NF.me"; - name = "NovelFullMe"; - site = "https://novelfull.me/"; - version = "1.0.0"; - filters?: Filters | undefined; - icon = "src/en/novelfullme/icon.png"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}popular?page=${pageNo}`; - - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".book-item").each((idx, ele) => { - const novelName = loadedCheerio(ele).find(".title").text(); - const novelCover = - "https:" + loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = - this.baseUrl + - loadedCheerio(ele).find(".title a").attr("href")?.substring(1); - - const novel = { name: novelName, cover: novelCover, url: novelUrl }; - - novels.push(novel); - }); - - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - - const result = await fetchApi(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let novel: Plugin.SourceNovel = { - url, - name: "", - cover: "", - author: "", - status: "", - genres: "", - summary: "", - chapters: [], - }; - - novel.name = loadedCheerio(".name h1").text().trim(); - - novel.cover = - "https:" + loadedCheerio(".img-cover img").attr("data-src"); - - novel.summary = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content" - ) - .text() - .trim(); - - novel.author = "Unknown"; - - novel.status = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span" - ) - .text() - ?.trim(); - - novel.genres = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2)" - ) - .text() - ?.replace("Genres :", "") - .replace(/[\s\n]+/g, " ") - .trim(); - - let chapters: Plugin.ChapterItem[] = []; - - const chaptersUrl = - novelUrl.replace(this.baseUrl, "https://novelfull.me/api/novels/") + - "/chapters?source=detail"; - - const chaptersRequest = await fetchApi(chaptersUrl); - const chaptersHtml = await chaptersRequest.text(); - - loadedCheerio = parseHTML(chaptersHtml); - - loadedCheerio("li").each(function () { - const chapterName = loadedCheerio(this) - .find(".chapter-title") - .text() - .trim(); - - const releaseDate = loadedCheerio(this) - .find(".chapter-update") - .text() - .trim(); - - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - chapters.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapters.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; - - const result = await fetchApi(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - loadedCheerio("#listen-chapter").remove(); - loadedCheerio("#google_translate_element").remove(); - - const chapterText = loadedCheerio(".chapter__content").html() || ''; - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}search?status=all&sort=views&q=${searchTerm}`; - - const result = await fetchApi(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".book-item").each((idx, ele) => { - const novelName = loadedCheerio(ele).find(".title").text(); - const novelCover = - "https:" + loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = - this.baseUrl + - loadedCheerio(ele).find(".title a").attr("href")?.substring(1); - - const novel = { name: novelName, cover: novelCover, url: novelUrl }; - - novels.push(novel); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - -} - -export default new NovelFull(); diff --git a/plugins/english/NovelOnline.ts b/plugins/english/NovelOnline.ts index 0de18b8..d113e65 100644 --- a/plugins/english/NovelOnline.ts +++ b/plugins/english/NovelOnline.ts @@ -1,50 +1,84 @@ import { load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { Filters, FilterTypes } from "@libs/filterInputs"; class NovelsOnline implements Plugin.PluginBase { id = "NO.net"; name = "novelsOnline"; site = "https://novelsonline.net"; - icon = "src/coverNotAvailable.jpg"; - filters?: Filters | undefined; + icon = "src/en/novelsonline/icon.png"; version = "1.0.0"; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - return []; /** TO DO */ + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + let link = this.site; + if (filters.genre.value !== '') + link += `/category/${filters.genre.value}/`; + else + link += `/top-novel/`; + + link += page + link += `?change_type=${filters.sort.value}` + + const body = await fetchApi(link).then((r) => r.text()); + + const loadedCheerio = parseHTML(body); + const novel: Plugin.NovelItem[] = []; + + loadedCheerio('.top-novel-block').each((i,el) => { + const novelName = loadedCheerio(el).find('h2').text(); + const novelCover = loadedCheerio(el).find('.top-novel-cover img').attr('src'); + const novelUrl = loadedCheerio(el).find('h2 a').attr('href'); + if (!novelUrl) return; + + novel.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }) + }) + return novel; } - async parseNovelAndChapters(novelUrl: string): Promise { - let novel: Plugin.SourceNovel = { - url: novelUrl, + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath).then((res) => res.text()); + let $ = parseHTML(result); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $("h1").text() || 'Untitled', + cover: $(".novel-cover").find("a > img").attr("src"), chapters: [], }; - const result = await fetchApi(novelUrl).then((res) => res.text()); - - let $ = parseHTML(result); + $('.novel-detail-item').each((i,el) => { + let detailName = $(el).find('h6').text(); + let detail = $(el).find('.novel-detail-body'); - novel.name = $("h1").text(); - novel.cover = $(".novel-cover").find("a > img").attr("src"); - novel.author = $( - "div.novel-details > div:nth-child(5) > div.novel-detail-body" - ) - .find("li") - .map((_, el) => $(el).text()) - .get() - .join(", "); - - novel.genres = $( - "div.novel-details > div:nth-child(2) > div.novel-detail-body" - ) - .find("li") - .map((_, el) => $(el).text()) - .get() - .join(","); - - novel.summary = $( - "div.novel-right > div > div:nth-child(1) > div.novel-detail-body" - ).text(); + switch(detailName){ + case "Description": + novel.summary = detail.text(); + break; + case "Genre": + novel.genres = detail.find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + break; + case "Author(s)": + novel.author = detail.find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + break; + case "Status": + novel.status = detail.text().trim(); + break; + } + }) novel.chapters = $("ul.chapter-chs > li > a") .map((_, el) => { @@ -53,22 +87,23 @@ class NovelsOnline implements Plugin.PluginBase { return { name: chapterName, - releaseTime: "", - url: chapterUrl, + path: chapterUrl?.replace(this.site, ''), } as Plugin.ChapterItem; }) .get(); return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl).then((res) => res.text()); - let loadedCheerio = parseHTML(result); + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const loadedCheerio = parseHTML(result); const chapterText = loadedCheerio("#contentall").html() || ""; return chapterText; } + async searchNovels(searchTerm: string, pageNo: number): Promise { const result = await fetchApi( "https://novelsonline.net/sResults.php", @@ -84,7 +119,7 @@ class NovelsOnline implements Plugin.PluginBase { }, ).then((res) => res.text()); - let $ = parseHTML(result); + const $ = parseHTML(result); const headers = $("li"); return headers @@ -93,23 +128,76 @@ class NovelsOnline implements Plugin.PluginBase { const novelUrl = $(h).find("a").attr("href"); const novelCover = $(h).find("img").attr("src"); - if (!novelUrl) { - return null; - } + if (!novelUrl) return return { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; }) .get() .filter((sr) => sr !== null) } + async fetchImage(url: string): Promise { return fetchFile(url); } + filters = { + sort: { + value: "top_rated", + label: "Sort by", + options: [ + { label: "Top Rated", value: "top_rated" }, + { label: "Most Viewed", value: "view" }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: "", + label: "Category", + options: [ + { label: "None", value: "" }, + { label: "Action", value: "action" }, + { label: "Adventure", value: "adventure" }, + { label: "Celebrity", value: "celebrity" }, + { label: "Comedy", value: "comedy" }, + { label: "Drama", value: "drama" }, + { label: "Ecchi", value: "ecchi" }, + { label: "Fantasy", value: "fantasy" }, + { label: "Gender Bender", value: "gender-bender" }, + { label: "Harem", value: "harem" }, + { label: "Historical", value: "historical" }, + { label: "Horror", value: "horror" }, + { label: "Josei", value: "josei" }, + { label: "Martial Arts", value: "martial-arts" }, + { label: "Mature", value: "mature" }, + { label: "Mecha", value: "mecha" }, + { label: "Mystery", value: "mystery" }, + { label: "Psychological", value: "psychological" }, + { label: "Romance", value: "romance" }, + { label: "School Life", value: "school-life" }, + { label: "Sci-fi", value: "sci-fi" }, + { label: "Seinen", value: "seinen" }, + { label: "Shotacon", value: "shotacon" }, + { label: "Shoujo", value: "shoujo" }, + { label: "Shoujo Ai", value: "shoujo-ai" }, + { label: "Shounen", value: "shounen" }, + { label: "Shounen Ai", value: "shounen-ai" }, + { label: "Slice of Life", value: "slice-of-life" }, + { label: "Sports", value: "sports" }, + { label: "Supernatural", value: "supernatural" }, + { label: "Tragedy", value: "tragedy" }, + { label: "Wuxia", value: "wuxia" }, + { label: "Xianxia", value: "xianxia" }, + { label: "Xuanhuan", value: "xuanhuan" }, + { label: "Yaoi", value: "yaoi" }, + { label: "Yuri", value: "yuri" } + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new NovelsOnline(); diff --git a/plugins/english/allnovelfull.ts b/plugins/english/allnovelfull.ts index 6530bb1..2c341f8 100644 --- a/plugins/english/allnovelfull.ts +++ b/plugins/english/allnovelfull.ts @@ -15,23 +15,20 @@ class AllNovelFullPlugin implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = []; loadedCheerio(".col-truyen-main .list-truyen .row").each((i,el) => { + const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href"); + if (!novelUrl) return; + const novelName = loadedCheerio(el) .find("h3.truyen-title > a") .text(); + let novelCover = loadedCheerio(el).find("img.cover").attr("src"); - let novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href"); - - if (!novelUrl) return; - - if (novelUrl && !isUrlAbsolute(novelUrl)) { - novelUrl = this.site + novelUrl; - } if (novelCover && !isUrlAbsolute(novelCover)){ novelCover = this.site + novelCover; } const novel = { - url: novelUrl, + path: novelUrl, name: novelName, cover: novelCover, }; @@ -53,41 +50,35 @@ class AllNovelFullPlugin implements Plugin.PluginBase { link += `?page=${pageNo}`; - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); + const result = await fetchApi(link); + const body = await result.text(); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const result = await fetchApi(url); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio(".book > img").attr("alt") || 'Untitled', + cover: this.site + loadedCheerio(".book > img").attr("src"), + summary: loadedCheerio(".desc-text").text().trim(), chapters: [], }; - novel.name = loadedCheerio(".book > img").attr("alt"); - - novel.cover = this.site + loadedCheerio(".book > img").attr("src"); - - novel.summary = loadedCheerio(".desc-text").text().trim(); - loadedCheerio(".info > div").each((i,el) => { const detailName = loadedCheerio(el).find("h3").text(); const detail = loadedCheerio(el) .find('a') .map((a,ex) => loadedCheerio(ex).text()) .toArray() - .join(', '); + .join(','); switch (detailName) { case 'Author:': @@ -106,7 +97,7 @@ class AllNovelFullPlugin implements Plugin.PluginBase { const getChapters = async (id: string) => { const chaptersUrl = this.site + "/ajax/chapter-option?novelId=" + id; - const data = await fetchApi(chaptersUrl, { headers }); + const data = await fetchApi(chaptersUrl); const chapters = await data.text(); loadedCheerio = parseHTML(chapters); @@ -115,16 +106,13 @@ class AllNovelFullPlugin implements Plugin.PluginBase { loadedCheerio("select > option").each((i,el) => { const chapterName = loadedCheerio(el).text(); - const releaseDate = null; - const cUrl = loadedCheerio(el).attr("value"); - const chapterUrl = this.site + cUrl + const chapterUrl = loadedCheerio(el).attr("value"); - if (!cUrl) return; + if (!chapterUrl ) return; chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl, }); }); @@ -138,9 +126,8 @@ class AllNovelFullPlugin implements Plugin.PluginBase { return novel; }; - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -152,11 +139,10 @@ class AllNovelFullPlugin implements Plugin.PluginBase { async searchNovels( searchTerm: string, - pageNo: number + page: number ): Promise { - const url = `${this.site}/search?keyword=${searchTerm}&page=${pageNo}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); + const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`; + const result = await fetchApi(url); const body = await result.text(); const loadedCheerio = parseHTML(body); diff --git a/plugins/english/bestlightnovel.ts b/plugins/english/bestlightnovel.ts index fd13fa6..8ba7f0e 100644 --- a/plugins/english/bestlightnovel.ts +++ b/plugins/english/bestlightnovel.ts @@ -1,52 +1,34 @@ -import { load as cheerioload } from "cheerio"; +import { CheerioAPI, load as parseHTML} from "cheerio"; import { Plugin } from "@typings/plugin"; import { defaultCover } from "@libs/defaultCover"; import { fetchFile } from "@libs/fetch"; import { NovelStatus } from "@libs/novelStatus"; +import { FilterTypes, Filters } from "@libs/filterInputs"; class BLN implements Plugin.PluginBase { - id = "BLN.com"; + id = "BLN"; name = "BestLightNovel"; icon = "src/en/bestlightnovel/icon.png"; site = "https://bestlightnovel.com/"; - filter?: undefined; version = "1.0.0"; - - async popularNovels( - pageNo: number, - options: Plugin.PopularNovelsOptions - ): Promise { - const url = - this.site + - "novel_list?type=topview&category=all&state=all&page=1" + - pageNo; - const result = await fetch(url); - if (!result.ok) { - console.error(await result.text()); - // TODO: Cloudflare protection or other error - return []; - } - const body = await result.text(); - - const loadedCheerio = cheerioload(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".update_item.list_category").each(function () { - const novelName = loadedCheerio(this).find("h3 > a").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h3 > a").attr("href"); + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio(".update_item.list_category").each((i,el) => { + const novelUrl = loadedCheerio(el).find("h3 > a").attr("href"); if (!novelUrl) { // TODO: Handle error console.error("No novel url!"); return; } + const novelName = loadedCheerio(el).find("h3 > a").text(); + const novelCover = loadedCheerio(el).find("img").attr("src"); + const novel = { name: novelName, - url: novelUrl, + path: novelUrl.replace(this.site, ''), cover: novelCover, }; @@ -55,126 +37,243 @@ class BLN implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetch(url); + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + let link = this.site + "novel_list?" + link += 'type=' + filters.type.value; + link += '&category=' + filters.category.value; + link += '&state=' + filters.status.value; + link += '&page=' + page; + + const result = await fetch(link); if (!result.ok) { console.error(await result.text()); - // TODO: Cloudflare protection - return { url, chapters: [] }; + // TODO: Cloudflare protection or other error + return []; } const body = await result.text(); - let loadedCheerio = cheerioload(body); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - let novel: Plugin.SourceNovel = { - url, - name: "", - cover: "", - author: "", - status: NovelStatus.Unknown, - genres: "", - summary: "", + async parseNovel(novelPath: string): Promise { + const result = await fetch(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio(".truyen_info_right h1").text().trim() || "", + cover: loadedCheerio(".info_image img").attr("src") || defaultCover, + summary: loadedCheerio("#noidungm").text()?.trim() || "", chapters: [], }; + loadedCheerio('ul.truyen_info_right > li').each(function() { + const detailName = loadedCheerio(this).find('span').text(); + const detail = loadedCheerio(this) + .find('a') + .map((a,ex) => loadedCheerio(ex).text()) + .toArray() + .join(', '); + + switch(detailName) { + case "Author(s): ": + novel.author = detail; + break; + case "GENRES: ": + novel.genres = detail; + break; + case "STATUS : ": + novel.status = + detail === "Ongoing" + ? NovelStatus.Ongoing + : detail === "Completed" + ? NovelStatus.Completed + : NovelStatus.Unknown; + } + }) - novel.name = loadedCheerio(".truyen_info_right h1").text().trim(); - novel.cover = - loadedCheerio(".info_image img").attr("src") || defaultCover; - novel.summary = loadedCheerio("#noidungm").text()?.trim(); - novel.author = loadedCheerio( - "#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(2) > a" - ) - .text() - ?.trim(); + const chapter: Plugin.ChapterItem[] = []; - let status = loadedCheerio( - "#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(4) > a" - ) - .text() - ?.trim(); - - novel.status = - status === "ONGOING" - ? NovelStatus.Ongoing - : status === "COMPLETED" - ? NovelStatus.Completed - : NovelStatus.Unknown; - - let novelChapters: Plugin.ChapterItem[] = []; - - loadedCheerio(".chapter-list div.row").each(function () { - const chapterName = loadedCheerio(this).find("a").text().trim(); - const releaseDate = loadedCheerio(this) - .find("span:nth-child(2)") + loadedCheerio(".chapter-list div.row").each((i, el) => { + const chapterName = loadedCheerio(el).find("a").text().trim(); + const releaseDate = loadedCheerio(el) + .find("span:last") .text() .trim(); - const chapterUrl = loadedCheerio(this).find("a").attr("href"); + const months = [ + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", + ].join("|"); + const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`,'i') + .exec(releaseDate); + if (!rx) return; + const year = 2000 + +rx[3]; + const month = months.indexOf(rx[1].toLowerCase()); + const day = +rx[2]; + + const chapterUrl = loadedCheerio(el).find("a").attr("href"); if (!chapterUrl) { // TODO: Handle error console.error("No chapter url!"); return; } - novelChapters.push({ + chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + releaseTime: new Date(year, month, day).toISOString(), + path: chapterUrl.replace(this.site, ''), }); }); - novel.chapters = novelChapters.reverse(); + novel.chapters = chapter.reverse(); return novel; } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; - const result = await fetch(url); + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); const body = await result.text(); - let loadedCheerio = cheerioload(body); + const loadedCheerio = parseHTML(body); const chapterText = loadedCheerio("#vung_doc").html() || ""; return chapterText; } + async searchNovels( searchTerm: string, - pageNo?: number | undefined + page: number ): Promise { - const url = `${this.site}search_novels/${searchTerm}`; + const url = `${this.site}search_novels/${searchTerm}?page=${page}`; const result = await fetch(url); const body = await result.text(); - let loadedCheerio = cheerioload(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".update_item.list_category").each(function () { - const novelName = loadedCheerio(this).find("h3 > a").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h3 > a").attr("href"); - - if (!novelUrl) { - // TODO: Handle error - console.error("No novel url!"); - return; - } - - const novel = { name: novelName, cover: novelCover, url: novelUrl }; - - novels.push(novel); - }); - - return novels; + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); } + async fetchImage(url: string): Promise { return await fetchFile(url); } + + filters = { + status: { + label: "Status", + value: "all", + options: [ + { label: "ALL", value: "all" }, + { label: "Completed", value: "completed" }, + { label: "Ongoing", value: "ongoing" }, + ], + type: FilterTypes.Picker, + }, + type: { + value: "topview", + label: "Type", + options: [ + { label: "Recently updated", value: "latest" }, + { label: "Newest", value: "newest" }, + { label: "Top view", value: "topview" }, + ], + type: FilterTypes.Picker, + }, + category: { + label: "Category", + value: "all", + options: [ + { label: "ALL", value: "all" }, + { label: "Action", value: "1" }, + { label: "Adventure", value: "2" }, + { label: "Animals", value: "65" }, + { label: "Arts", value: "40" }, + { label: "Biographies", value: "41" }, + { label: "Business", value: "42" }, + { label: "Chinese", value: "3" }, + { label: "Comedy", value: "4" }, + { label: "Computers", value: "43" }, + { label: "Crafts, Hobbies", value: "45" }, + { label: "Drama", value: "5" }, + { label: "Education", value: "46" }, + { label: "English", value: "6" }, + { label: "Entertainment", value: "47" }, + { label: "Fantasy", value: "7" }, + { label: "Fiction", value: "48" }, + { label: "Gender Bender", value: "8" }, + { label: "Harem", value: "9" }, + { label: "Historical", value: "10" }, + { label: "History", value: "49" }, + { label: "Home", value: "50" }, + { label: "Horror", value: "11" }, + { label: "Humor", value: "51" }, + { label: "Investing", value: "52" }, + { label: "Josei", value: "12" }, + { label: "Korean", value: "13" }, + { label: "Literature", value: "53" }, + { label: "Lolicon", value: "14" }, + { label: "Martial Arts", value: "15" }, + { label: "Mature", value: "16" }, + { label: "Mecha", value: "17" }, + { label: "Memoirs", value: "54" }, + { label: "Mystery", value: "18" }, + { label: "Original", value: "19" }, + { label: "Other Books", value: "66" }, + { label: "Philosophy", value: "55" }, + { label: "Photography", value: "56" }, + { label: "Politics", value: "57" }, + { label: "Professional", value: "58" }, + { label: "Psychological", value: "20" }, + { label: "Reference", value: "59" }, + { label: "Reincarnation", value: "21" }, + { label: "Religion", value: "60" }, + { label: "Romance", value: "22" }, + { label: "School Life", value: "23" }, + { label: "School Stories", value: "67" }, + { label: "Sci-Fi", value: "24" }, + { label: "Seinen", value: "25" }, + { label: "Short Stories", value: "68" }, + { label: "Shotacon", value: "26" }, + { label: "Shoujo", value: "27" }, + { label: "Shoujo Ai", value: "28" }, + { label: "Shounen", value: "29" }, + { label: "Shounen Ai", value: "30" }, + { label: "Slice Of Life", value: "31" }, + { label: "Smut", value: "32" }, + { label: "Social Science", value: "61" }, + { label: "Spirituality", value: "62" }, + { label: "Sports", value: "33" }, + { label: "Supernatural", value: "34" }, + { label: "Teaser", value: "69" }, + { label: "Technical", value: "63" }, + { label: "Technology", value: "64" }, + { label: "Tragedy", value: "35" }, + { label: "Virtual Reality", value: "36" }, + { label: "Wuxia", value: "37" }, + { label: "Xianxia", value: "38" }, + { label: "Xuanhuan", value: "39" }, + + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new BLN(); diff --git a/plugins/english/comrademao.ts b/plugins/english/comrademao.broken.ts similarity index 100% rename from plugins/english/comrademao.ts rename to plugins/english/comrademao.broken.ts diff --git a/plugins/english/divinedaolibrary.ts b/plugins/english/divinedaolibrary.ts index ca02258..d0f2c14 100644 --- a/plugins/english/divinedaolibrary.ts +++ b/plugins/english/divinedaolibrary.ts @@ -10,23 +10,24 @@ class DDLPlugin implements Plugin.PluginBase { site = "https://www.divinedaolibrary.com/"; version = "1.0.0"; icon = "src/en/divinedaolibrary/icon.png"; + filters?: undefined; parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string){ let novels: Plugin.NovelItem[] = []; loadedCheerio("#main") .find("li") - .each(function () { - const novelName = loadedCheerio(this).find("a").text(); + .each((i,el) => { + const novelName = loadedCheerio(el).find("a").text(); const novelCover = defaultCover; - const novelUrl = loadedCheerio(this).find(" a").attr("href"); + const novelUrl = loadedCheerio(el).find(" a").attr("href"); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; novels.push(novel); @@ -42,38 +43,31 @@ class DDLPlugin implements Plugin.PluginBase { async popularNovels( pageNo: number, - { filters }: Plugin.PopularNovelsOptions + options: Plugin.PopularNovelsOptions ): Promise { let link = this.site + "novels"; - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); + const body = await fetchApi(link).then((res) => res.text()); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio("h1.entry-title").text().trim() || "Untitled", + cover: + loadedCheerio(".entry-content").find("img").attr("data-ezsrc") || + defaultCover, chapters: [], }; - novel.name = loadedCheerio("h1.entry-title").text().trim(); - - novel.cover = - loadedCheerio(".entry-content").find("img").attr("data-ezsrc") || - defaultCover; - novel.summary = loadedCheerio("#main > article > div > p:nth-child(6)") .text() .trim(); @@ -87,17 +81,15 @@ class DDLPlugin implements Plugin.PluginBase { loadedCheerio("#main") .find("li > span > a") - .each(function () { - const chapterName = loadedCheerio(this).text().trim(); - const releaseDate = null; - const chapterUrl = loadedCheerio(this).attr("href"); + .each((i, el) => { + const chapterName = loadedCheerio(el).text().trim(); + const chapterUrl = loadedCheerio(el).attr("href"); if (!chapterUrl) return; chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl.replace(this.site, ''), }); }); @@ -106,9 +98,8 @@ class DDLPlugin implements Plugin.PluginBase { return novel; }; - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -131,9 +122,8 @@ class DDLPlugin implements Plugin.PluginBase { pageNo: number ): Promise { let url = this.site + "novels"; - - const headers = new Headers(); - const result = await fetchApi(url, { headers }); + + const result = await fetchApi(url); const body = await result.text(); const loadedCheerio = parseHTML(body); diff --git a/plugins/english/earlynovel.ts b/plugins/english/earlynovel.ts index 109ad21..59cee81 100644 --- a/plugins/english/earlynovel.ts +++ b/plugins/english/earlynovel.ts @@ -2,9 +2,8 @@ import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { FilterTypes, Filters } from "@libs/filterInputs"; import { Plugin } from "@typings/plugin"; -import { defaultCover } from "@libs/defaultCover"; -class EarlyNovelPlugin implements Plugin.PluginBase { +class EarlyNovelPlugin implements Plugin.PagePlugin { id = "earlynovel"; name = "Early Novel"; version = "1.0.0"; @@ -16,27 +15,43 @@ class EarlyNovelPlugin implements Plugin.PluginBase { loadedCheerio(".col-truyen-main > .list-truyen > .row").each( (i, el) => { - const nUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href"); - - const novelUrl = this.site + nUrl + const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href"); const novelName = loadedCheerio(el) .find("h3.truyen-title > a") .text(); - const novelCover = loadedCheerio(el).find("img.cover").attr("src"); - console.log(novelCover); + const novelCover = loadedCheerio(el).find('.lazyimg').attr("data-image"); - if (!nUrl) return; + if (!novelUrl) return; novels.push({ - url: novelUrl, + path: novelUrl, name: novelName, cover: novelCover, }); } ); - return novels; }; + parseChapters(loadedCheerio:CheerioAPI){ + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio("ul.list-chapter > li").each((i,el) => { + const chapterName = loadedCheerio(el) + .find(".chapter-text") + .text() + .trim(); + const chapterUrl = loadedCheerio(el) + .find("a") + .attr("href") + ?.slice(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl + })}); + return chapter; + } + async popularNovels( pageNo: number, { filters }: Plugin.PopularNovelsOptions @@ -50,33 +65,32 @@ class EarlyNovelPlugin implements Plugin.PluginBase { link += `?page=${pageNo}`; - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); + const body = await fetchApi(link).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + console.log(body); return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); + loadedCheerio('.glyphicon-menu-right').closest('li').remove(); + const pagenav = loadedCheerio('.page-nav').prev().find('a'); + const lastPageStr = pagenav.attr('title')?.match(/(\d+)/); + const lastPage = Number(lastPageStr?.[1] || "0"); - const novel: Plugin.SourceNovel = { - url, + const novel: Plugin.SourceNovel & {totalPages: number}= { + path: novelPath, + name: loadedCheerio(".book > img").attr("alt") || "Untitled", + cover: loadedCheerio(".book > img").attr("src"), + summary: loadedCheerio(".desc-text").text().trim(), chapters: [], + totalPages: lastPage }; - novel.name = loadedCheerio(".book > img").attr("alt"); - - novel.cover = loadedCheerio(".book > img").attr("src"); - - novel.summary = loadedCheerio(".desc-text").text().trim(); - loadedCheerio(".info > div > h3").each(function () { let detailName = loadedCheerio(this).text(); let detail = loadedCheerio(this) @@ -98,58 +112,20 @@ class EarlyNovelPlugin implements Plugin.PluginBase { } }); - //! Doesn't work since there are multiple pages and can't find source API - //# Since cannot find sourceAPI i try similar function to lightnovelpub - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - - const lastPageStr = loadedCheerio('a:contains("Last ")') - .attr("title") - ?.match(/(\d+)/g); - const lastPage = Number(lastPageStr?.[1] || "0"); - - const getChapters = async () => { - const chapter: Plugin.ChapterItem[] = []; - for (let i = 1; i <= lastPage; i++) { - const chaptersUrl = `${novelUrl}?page=${i}`; - const data = await fetchApi(chaptersUrl, { headers }); - const chapters = await data.text(); - - loadedCheerio = parseHTML(chapters); - - loadedCheerio("ul.list-chapter > li").each((i,el) => { - const chapterName = loadedCheerio(el) - .find(".chapter-text") - .text() - .trim(); - const releaseDate = null; - const cUrl = loadedCheerio(el) - .find("a") - .attr("href") - ?.slice(1); - if (!cUrl) return; - const chapterUrl = this.site + cUrl; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - await delay(1000); - } - - return chapter; - } - - novel.chapters = await getChapters(); - + novel.chapters = this.parseChapters(loadedCheerio); return novel; - }; + } - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '?page=' + page; + const body = await fetchApi(url).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return {chapters} + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -164,8 +140,7 @@ class EarlyNovelPlugin implements Plugin.PluginBase { pageNo: number ): Promise { const url = `${this.site}search?keyword=${searchTerm}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); + const result = await fetchApi(url); const body = await result.text(); const loadedCheerio = parseHTML(body); diff --git a/plugins/english/faqwikius.ts b/plugins/english/faqwikius.ts index 18ee7ff..6fe8409 100644 --- a/plugins/english/faqwikius.ts +++ b/plugins/english/faqwikius.ts @@ -14,37 +14,39 @@ class FaqWikiUs implements Plugin.PluginBase { page: number, { showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { - const sort = showLatestNovels - ? "" - : ""; - - const body = await fetchApi(this.site + sort).then((res) => res.text()); + + const body = await fetchApi(this.site).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = loadedCheerio(".wp-block-table > table > tbody > tr") - .map((index, element) => { - const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,''); - let cover = loadedCheerio(element).find("img").attr("data-ezsrc"); + .map((index, element) => { + const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,''); + let cover = loadedCheerio(element).find("img").attr("data-ezsrc"); - // Remove the appended query string - if (cover) { - cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! - } + // Remove the appended query string + if (cover) { + cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! + } - const url = "" + loadedCheerio(element).find("a").attr("href"); + const path = "" + loadedCheerio(element).find("a").attr("href")?.toString().replace(this.site, "").trim(); - return { name, cover, url }; - }) - .get(); + + return { name, cover, path }; + }) + .get() + .filter((novel) => novel.name && novel.path); return novels; } - async parseNovelAndChapters(url: string): Promise { - const body = await fetchApi(url).then((res) => res.text()); + async parseNovel(path: string): Promise { + const body = await fetchApi(this.site + path).then((res) => res.text()); const loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel = { url }; + const novel: Plugin.SourceNovel = { + path, + name: "" + }; novel.name = loadedCheerio(".entry-title").text().replace("Novel – All Chapters", "").trim(); let cover = loadedCheerio(".wp-block-image").find("img").attr("data-ezsrc"); @@ -60,7 +62,7 @@ class FaqWikiUs implements Plugin.PluginBase { const chapters: Plugin.ChapterItem[] = loadedCheerio(".lcp_catlist > li > a") .map((chapterIndex, element) => ({ name: loadedCheerio(element).text().replace(novel.name + "", "").replace("Novel" + "", "").trim(), - url: ""+loadedCheerio(element).attr("href"), + path: ""+loadedCheerio(element).attr("href")?.toString().replace(this.site, "").trim(), releaseTime: null, chapterNumber: chapterIndex + 1, })) @@ -70,8 +72,8 @@ class FaqWikiUs implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); loadedCheerio("span").remove(); @@ -85,8 +87,8 @@ class FaqWikiUs implements Plugin.PluginBase { return text; }).get().join('\n\n'); } - - else { + + else { // Multiple paragraphs case chapterContent = chapterParagraphs.map((index, element) => { const text = loadedCheerio(element).text().trim(); @@ -95,33 +97,30 @@ class FaqWikiUs implements Plugin.PluginBase { } return chapterContent; - } +} - async searchNovels( - searchTerm: string, - ): Promise { + + async searchNovels(searchTerm: string): Promise { const body = await fetchApi(this.site).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = loadedCheerio(".wp-block-table > table > tbody > tr") - .map((index, element) => { - const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,''); - let cover = loadedCheerio(element).find("img").attr("data-ezsrc"); + .map((index, element) => { + const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,''); + let cover = loadedCheerio(element).find("img").attr("data-ezsrc"); - // Remove the appended query string - if (cover) { - cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! - } + // Remove the appended query string + if (cover) { + cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! + } - const url = "" + loadedCheerio(element).find("a").attr("href"); + const path = "" + loadedCheerio(element).find("a").attr("href")?.toString().replace(this.site, "").trim(); - return { name, cover, url }; - }) - .get(); - - const normalizedQuery = searchTerm.toLowerCase(); - return novels.filter(novel => novel.name.toLowerCase().includes(normalizedQuery) - ); + return { name, cover, path }; + }) + .get().filter((novel) => novel.name && novel.path); + const normalizedQuery = searchTerm.toLowerCase(); + return novels.filter(novel => novel.name.toLowerCase().includes(normalizedQuery)); return novels; } diff --git a/plugins/english/freenovelupdates.broken.ts b/plugins/english/freenovelupdates.broken.ts deleted file mode 100644 index 99da292..0000000 --- a/plugins/english/freenovelupdates.broken.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; -import { Novel, Plugin } from "@typings/plugin"; - -export const id = "freenovelupdates"; -export const name = "Free Novel Updates (Broken)"; -export const version = "1.0.0"; -export const icon = "src/en/freenovelupdates/icon.png"; -export const site = "https://www.freenovelupdates.com"; - - -const pluginId = id; -const baseUrl = site; - -export const popularNovels: Plugin.popularNovels = async function (page) { - const url = `${baseUrl}genres/light-novel-1002`; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let novels: Novel.Item[] = []; - - loadedCheerio(".books-item").each(function () { - let novelUrl = loadedCheerio(this).find("a").attr("href"); - - if (novelUrl && !isUrlAbsolute(novelUrl)) { - novelUrl = baseUrl + novelUrl; - } - - if (novelUrl) { - const novelName = loadedCheerio(this).find(".title").text().trim(); - let novelCover = loadedCheerio(this).find("img").attr("src"); - - if (novelCover && !isUrlAbsolute(novelCover)) { - novelCover = baseUrl + novelCover; - } - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - } - }); - - return novels; -}; - -export const parseNovelAndChapters: Plugin.parseNovelAndChapters = - async function (novelUrl) { - const url = novelUrl; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - const novel: Novel.instance = { - url, - chapters: [], - }; - - novel.name = loadedCheerio("h1").text(); - - let novelCover = loadedCheerio(".img > img").attr("src"); - - novel.cover = novelCover - ? isUrlAbsolute(novelCover) - ? novelCover - : baseUrl + novelCover - : undefined; - - novel.summary = loadedCheerio(".description-content").text().trim(); - - novel.author = loadedCheerio(".author").text().trim(); - - novel.genres = loadedCheerio(".category").text().trim(); - - novel.status = loadedCheerio(".status").text().trim(); - - loadedCheerio(".chapter").each(function () { - let chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (chapterUrl && !isUrlAbsolute(chapterUrl)) { - chapterUrl = baseUrl + chapterUrl; - } - - if (chapterUrl) { - const chapterName = loadedCheerio(this).find("a").text().trim(); - const releaseDate = null; - - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; - - novel.chapters?.push(chapter); - } - }); - - return novel; - }; - -export const parseChapter: Plugin.parseChapter = async function (chapterUrl) { - const body = await fetchApi(chapterUrl, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio(".content").html() || ""; - - return chapterText; -}; - -export const searchNovels: Plugin.searchNovels = async function (searchTerm) { - return []; -}; - -export const fetchImage: Plugin.fetchImage = async function (url) { - const headers = { - Referer: baseUrl, - }; - return await fetchFile(url, { headers: headers }); -}; diff --git a/plugins/english/freewebnovel.ts b/plugins/english/freewebnovel.ts index 1e3b376..6d0673f 100644 --- a/plugins/english/freewebnovel.ts +++ b/plugins/english/freewebnovel.ts @@ -23,22 +23,27 @@ class FreeWebNovel implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = loadedCheerio(".li-row") .map((index, element) => ({ - name: loadedCheerio(element).find(".tit").text(), + name: loadedCheerio(element).find(".tit").text() || "", cover: loadedCheerio(element).find("img").attr("src"), - url: this.site + loadedCheerio(element).find("h3 > a").attr("href"), + path: loadedCheerio(element).find("h3 > a").attr("href") || "", })) - .get(); + .get() + .filter((novel) => novel.name && novel.path); return novels; } - async parseNovelAndChapters(url: string): Promise { - const body = await fetchApi(url).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel = { url }; - novel.name = loadedCheerio("h1.tit").text(); - novel.cover = loadedCheerio(".pic > img").attr("src"); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio("h1.tit").text(), + cover: loadedCheerio(".pic > img").attr("src"), + summary: loadedCheerio(".inner").text().trim(), + }; + novel.genres = loadedCheerio("[title=Genre]") .next() .text() @@ -54,12 +59,14 @@ class FreeWebNovel implements Plugin.PluginBase { .text() .replace(/[\t\n]/g, ""); - novel.summary = loadedCheerio(".inner").text().trim(); - const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a") .map((chapterIndex, element) => ({ - name: loadedCheerio(element).attr("title") || "Chapter " + (chapterIndex + 1), - url: this.site + loadedCheerio(element).attr("href"), + name: + loadedCheerio(element).attr("title") || + "Chapter " + (chapterIndex + 1), + path: + loadedCheerio(element).attr("href") || + novelPath.replace(".html", "/chapter-" + (chapterIndex + 1) + ".html"), releaseTime: null, chapterNumber: chapterIndex + 1, })) @@ -69,17 +76,15 @@ class FreeWebNovel implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const chapterText = loadedCheerio("div.txt").html() || ""; return chapterText; } - async searchNovels( - searchTerm: string, - ): Promise { + async searchNovels(searchTerm: string): Promise { const body = await fetchApi(this.site + "/search/", { headers: { "Content-Type": "application/x-www-form-urlencoded", @@ -87,17 +92,18 @@ class FreeWebNovel implements Plugin.PluginBase { Origin: this.site, }, method: "POST", - body: "searchkey=" + encodeURIComponent(searchTerm) + body: "searchkey=" + encodeURIComponent(searchTerm), }).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con") .map((index, element) => ({ - name: loadedCheerio(element).find(".tit").text(), + name: loadedCheerio(element).find(".tit").text() || "", cover: loadedCheerio(element).find(".pic > a > img").attr("src"), - url: this.site + loadedCheerio(element).find("h3 > a").attr("href"), + path: loadedCheerio(element).find("h3 > a").attr("href") || "", })) - .get(); + .get() + .filter((novel) => novel.name && novel.path); return novels; } diff --git a/plugins/english/genesistl.ts b/plugins/english/genesistl.ts deleted file mode 100644 index 4b65a61..0000000 --- a/plugins/english/genesistl.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import dayjs from "dayjs"; -class Genesis implements Plugin.PluginBase { - id = "genesistls"; - name = "GenesisTLs"; - icon = "src/en/genesistls/icon.png"; - site = "https://genesistls.com/"; - version = "1.0.0"; - parseNovels(loadedCheerio: CheerioAPI) { - const novels: Plugin.NovelItem[] = []; - loadedCheerio("article.bs").each(function () { - const novelName = loadedCheerio(this).find(".ntitle").text().trim(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novelUrl = loadedCheerio(this).find("a").attr("href"); - if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - novels.push(novel); - }); - return novels; - } - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}series/?page=${pageNo}`; - if (filters.genres.value.length) - link += filters.genres.value.map((i) => `&genre[]=${i}`).join(""); - if (filters.type.value.length) - link += filters.type.value.map((i) => `&type[]=${i}`).join(""); - link += "&status=" + filters.status.value; - link += "&order=" + filters.order.value; - const body = await fetchApi(link).then((result) => - result.text() - ); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetchApi(url); - const body = await result.text(); - let loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - novel.name = loadedCheerio(".entry-title").text(); - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - loadedCheerio("div.spe > span").each(function () { - const detailName = loadedCheerio(this).find("b").text().trim(); - const detail = loadedCheerio(this) - .find("b") - .remove() - .end() - .text() - .trim(); - switch (detailName) { - case "Author:": - novel.author = detail; - break; - case "Status:": - novel.status = detail; - break; - } - }); - novel.genres = loadedCheerio(".genxed") - .text() - .trim() - .replace(/\s/g, ","); - loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove(); - novel.summary = loadedCheerio('div[itemprop="description"]') - .find("br") - .replaceWith("\n") - .end() - .text(); - let chapter: Plugin.ChapterItem[] = []; - loadedCheerio(".eplister") - .find("li") - .each(function () { - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - if (!chapterUrl) return; - const chapterpaid = loadedCheerio(this).find('.epl-price').text(); - if (chapterpaid !== "Free") return - const chapterName = - loadedCheerio(this).find(".epl-num").text() + - " - " + - loadedCheerio(this).find(".epl-title").text(); - const releaseDate = loadedCheerio(this) - .find(".epl-date") - .text() - .trim(); - - - if (!releaseDate) return; - const months = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", - ]; - const rx = new RegExp( - `(${months.join("|")}).*? (\\d{1,2}).*?(\\d{4})`, - "i" - ).exec(releaseDate); - if (!rx) return; - const year = +rx[3]; - const month = months.indexOf(rx[1].toLowerCase()); - const day = +rx[2]; - - if (month < 0) return; - - chapter.push({ - name: chapterName, - releaseTime: new Date(year, month, day).toISOString(), - url: chapterUrl, - }); - - }); - novel.chapters = chapter.reverse(); - return novel; - - } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); - const body = await result.text(); - const loadedCheerio = parseHTML(body); - let chapterText = loadedCheerio(".epcontent").html() || ""; - return chapterText; - } - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const url = `${this.site}page/${pageNo}/?s=${searchTerm}`; - - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - filters = { - order: { - label: "Sort By", - options: [ - { label: "Default", value: "" }, - { label: "A-Z", value: "title" }, - { label: "Z-A", value: "titlereverse" }, - { label: "Latest Update", value: "update" }, - { label: "Latest Added", value: "latest" }, - { label: "Popular", value: "popular" }, - ], - type: FilterTypes.Picker, - value: "", - }, - status: { - label: "Status", - options: [ - { label: "All", value: "" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Hiatus", value: "hiatus" }, - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - value: "", - }, - type: { - label: "Type", - options: [ - { label: "korean novel", value: "korean-novel" }, - { label: "Web Novel", value: "web-novel" }, - ], - type: FilterTypes.CheckboxGroup, - value: [], - }, - genres: { - label: "Genres", - options: [ - - { label: "Academy", value: "academy" }, - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Another World", value: "another-world" }, - { label: "Comdey", value: "comdey" }, - { label: "Comedy", value: "comedy" }, - { label: "Dark Fantasy", value: "dark-fantasy" }, - { label: "Drama", value: "drama" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Fantasy Fusion", value: "fantasy-fusion" }, - { label: "Harem", value: "harem" }, - { label: "Historical", value: "historical" }, - { label: "Horror", value: "horror" }, - { label: "Hunter", value: "hunter" }, - { label: "Light Novel", value: "light-novel" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Misunderstanding", value: "misunderstanding" }, - { label: "Modern", value: "modern" }, - { label: "Munchkin", value: "munchkin" }, - { label: "Murim", value: "murim" }, - { label: "mystery", value: "mystery" }, - { label: "No Harem", value: "no-harem" }, - { label: "NO NTR", value: "no-ntr" }, - { label: "obsession", value: "obsession" }, - { label: "Possession", value: "possession" }, - { label: "Psychological", value: "psychological" }, - { label: "Regression", value: "regression" }, - { label: "Regret", value: "regret" }, - { label: "reincarnation", value: "reincarnation" }, - { label: "Romance", value: "romance" }, - { label: "School Life", value: "school-life" }, - { label: "Seinen", value: "seinen" }, - { label: "Slice of life", value: "slice-of-life" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Transmigrated to Game", value: "transmigrated-to-game" }, - { label: "Transmigration", value: "transmigration" }, - ], - type: FilterTypes.CheckboxGroup, - value: [], - }, - } satisfies Filters; -} -export default new Genesis(); - diff --git a/plugins/english/lightnovelpub.ts b/plugins/english/lightnovelpub.ts index eed37ab..ed64368 100644 --- a/plugins/english/lightnovelpub.ts +++ b/plugins/english/lightnovelpub.ts @@ -1,25 +1,31 @@ import { load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { Filters, FilterTypes } from "@libs/filterInputs"; +import dayjs from "dayjs"; - -class LightNovelPub implements Plugin.PluginBase { +class LightNovelPub implements Plugin.PagePlugin { id = "lightnovelpub"; name = "LightNovelPub"; version = "1.0.0"; icon = "src/en/lightnovelpub/icon.png"; - filters?: Filters | undefined; site = "https://www.lightnovelpub.com/"; - baseUrl = this.site; headers = { Accept: "application/json", "Content-Type": "application/json", }; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}browse/all/popular/all/${pageNo}`; - const body = await fetchApi(url).then((r) => r.text()); + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + let link = `${this.site}browse/`; + link += `${filters.genres.value}/`; + link += `${filters.order.value}/`; + link += `${filters.status.value}/`; + link += page; + + const body = await fetchApi(link).then((r) => r.text()); const loadedCheerio = parseHTML(body); @@ -34,16 +40,16 @@ class LightNovelPub implements Plugin.PluginBase { .trim(); const novelCover = loadedCheerio(ele).find("img").attr("data-src"); const novelUrl = - this.baseUrl + loadedCheerio(ele) .find(".novel-title > a") .attr("href") ?.substring(1); + if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl, }; novels.push(novel); @@ -51,67 +57,62 @@ class LightNovelPub implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const body = await fetchApi(url).then((r) => r.text()); - let loadedCheerio = parseHTML(body); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((r) => r.text()); - const novel: Plugin.SourceNovel = { - url, + const loadedCheerio = parseHTML(body); + const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10); + + const novel: Plugin.SourceNovel & {totalPages: number}= { + path: novelPath, + name: loadedCheerio("h1.novel-title").text().trim() || "Untitled", + cover: loadedCheerio("figure.cover > img").attr("data-src"), + author: loadedCheerio(".author > a > span").text(), + summary: loadedCheerio(".summary > .content").text().trim(), + status: loadedCheerio(".header-stats span:last strong").text(), + totalPages: Math.ceil(totalChapters/100), chapters: [], }; - novel.name = loadedCheerio("h1.novel-title").text().trim(); + novel.genres = loadedCheerio('.categories ul li') + .map((a,ex) => loadedCheerio(ex).text().trim()) + .toArray() + .join(','); - novel.cover = loadedCheerio("figure.cover > img").attr("data-src"); - - novel.genres = ""; - - loadedCheerio(".categories > ul > li > a").each(function () { - novel.genres += loadedCheerio(this).text() + ","; - }); - - novel.genres = novel.genres.slice(0, -1); - - loadedCheerio("div.header-stats > span").each(function () { - if (loadedCheerio(this).find("small").text() === "Status") { - novel.status = loadedCheerio(this).find("strong").text(); - } - }); - - novel.author = loadedCheerio(".author > a > span").text(); - - novel.summary = loadedCheerio(".summary > .content").text().trim(); - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - await delay(1000); - const chapterListUrl = novelUrl + '/chapters?chorder=desc'; - - const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); - const loadedChapterList = parseHTML(chapterListBody); - - - const lastNumber = parseInt( - loadedChapterList('ul.chapter-list > li:nth-child(1)').attr('data-chapterno') ?? '0' - ); - const lastChapterUrl = loadedChapterList('ul.chapter-list > li:nth-child(1) > a').attr('href'); - - if(lastChapterUrl){ - const chapterUrlPrefix = this.baseUrl + lastChapterUrl.split('/').slice(1, -1).join('/'); - const chapters: Plugin.ChapterItem[] = []; - for(let i = 1; i <= lastNumber; i++){ - chapters.push({ - chapterNumber: i, - name: 'Chapter ' + i, - url: chapterUrlPrefix + '/chapter-' + i, - }) - } - novel.chapters = chapters; - } return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); + + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '/chapters/page-' + page; + const body = await fetchApi(url).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.chapter-list li').each(function () { + const chapterName = + 'Chapter ' + + loadedCheerio(this).find('.chapter-no').text().trim() + + ' - '; + loadedCheerio(this).find('.chapter-title').text().trim(); + + const releaseDate = loadedCheerio(this) + .find('.chapter-update') + .attr('datetime'); + + const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, + releaseTime: dayjs(releaseDate).toISOString(), + })}); + const chapters = chapter; + return {chapters}; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); const loadedCheerio = parseHTML(body); @@ -119,9 +120,10 @@ class LightNovelPub implements Plugin.PluginBase { return chapterText; } + async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}lnsearchlive`; - const link = `${this.baseUrl}search`; + const url = `${this.site}lnsearchlive`; + const link = `${this.site}search`; const response = await fetchApi(link).then((r) => r.text()); const token = parseHTML(response); let verifytoken = token("#novelSearchForm > input").attr("value"); @@ -152,22 +154,93 @@ class LightNovelPub implements Plugin.PluginBase { .text() .trim(); const novelCover = loadedCheerio(ele).find("img").attr("src"); - const novelUrl = - this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.substring(1); - + const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1); + if (!novelUrl) return; novels.push({ name: novelName, - url: novelUrl, + path: novelUrl, cover: novelCover, }); }); return novels; } + async fetchImage(url: string): Promise { return fetchFile(url, {headers: this.headers}); } + filters = { + order: { + value: "popular", + label: "Order by", + options: [ + { label: "New", value: "new" }, + { label: "Popular", value: "popular" }, + { label: "Updates", value: "updated" }, + ], + type: FilterTypes.Picker, + }, + status: { + value: "all", + label: "Status", + options: [ + { label: "All", value: "all" }, + { label: "Completed", value: "completed" }, + { label: "Ongoing", value: "ongoing" } + ], + type: FilterTypes.Picker, + }, + genres: { + value: "all", + label: "Genre", + options: [ + { label: "All", value: "all" }, + { label: "Action", value: "action" }, + { label: "Adventure", value: "adventure" }, + { label: "Drama", value: "drama" }, + { label: "Fantasy", value: "fantasy" }, + { label: "Harem", value: "harem" }, + { label: "Martial Arts", value: "martial-arts" }, + { label: "Mature", value: "mature" }, + { label: "Romance", value: "romance" }, + { label: "Tragedy", value: "tragedy" }, + { label: "Xuanhuan", value: "xuanhuan" }, + { label: "Ecchi", value: "ecchi" }, + { label: "Comedy", value: "comedy" }, + { label: "Slice of Life", value: "slice-of-life" }, + { label: "Mystery", value: "mystery" }, + { label: "Supernatural", value: "supernatural" }, + { label: "Psychological", value: "psychological" }, + { label: "Sci-fi", value: "sci-fi" }, + { label: "Xianxia", value: "xianxia" }, + { label: "School Life", value: "school-life" }, + { label: "Josei", value: "josei" }, + { label: "Wuxia", value: "wuxia" }, + { label: "Shounen", value: "shounen" }, + { label: "Horror", value: "horror" }, + { label: "Mecha", value: "mecha" }, + { label: "Historical", value: "historical" }, + { label: "Shoujo", value: "shoujo" }, + { label: "Adult", value: "adult" }, + { label: "Seinen", value: "seinen" }, + { label: "Sports", value: "sports" }, + { label: "Lolicon", value: "lolicon" }, + { label: "Gender Bender", value: "gender-bender" }, + { label: "Shounen Ai", value: "shounen-ai" }, + { label: "Yaoi", value: "yaoi" }, + { label: "Video Games", value: "video-games" }, + { label: "Smut", value: "smut" }, + { label: "Magical Realism", value: "magical-realism" }, + { label: "Eastern Fantasy", value: "eastern-fantasy" }, + { label: "Contemporary Romance", value: "contemporary-romance" }, + { label: "Fantasy Romance", value: "fantasy-romance" }, + { label: "Shoujo Ai", value: "shoujo-ai" }, + { label: "Yuri", value: "yuri" }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new LightNovelPub(); \ No newline at end of file diff --git a/plugins/english/lightnovelworld.ts b/plugins/english/lightnovelworld.ts index 7893044..e0081a2 100644 --- a/plugins/english/lightnovelworld.ts +++ b/plugins/english/lightnovelworld.ts @@ -1,25 +1,31 @@ import { load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { Filters, FilterTypes } from "@libs/filterInputs"; +import dayjs from "dayjs"; - -class LightNovelWorld implements Plugin.PluginBase { +class LightNovelWorld implements Plugin.PagePlugin { id = "lightnovelworld"; name = "LightNovelWorld"; site = "https://www.webnovelworld.org/"; version = "1.0.0"; - filters?: Filters | undefined; icon = "src/en/lightnovelworld/icon.png"; - baseUrl = this.site; headers = { Accept: "application/json", "Content-Type": "application/json", }; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}browse/all/popular/all/${pageNo}`; - const body = await fetchApi(url).then((r) => r.text()); + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + let link = `${this.site}browse/`; + link += `${filters.genres.value}/`; + link += `${filters.order.value}/`; + link += `${filters.status.value}/`; + link += page; + + const body = await fetchApi(link).then((r) => r.text()); const loadedCheerio = parseHTML(body); @@ -34,16 +40,16 @@ class LightNovelWorld implements Plugin.PluginBase { .trim(); const novelCover = loadedCheerio(ele).find("img").attr("data-src"); const novelUrl = - this.baseUrl + loadedCheerio(ele) .find(".novel-title > a") .attr("href") ?.substring(1); + if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl, }; novels.push(novel); @@ -51,67 +57,62 @@ class LightNovelWorld implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const body = await fetchApi(url).then((r) => r.text()); - let loadedCheerio = parseHTML(body); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((r) => r.text()); - const novel: Plugin.SourceNovel = { - url, + const loadedCheerio = parseHTML(body); + const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10); + + const novel: Plugin.SourceNovel & {totalPages: number}= { + path: novelPath, + name: loadedCheerio("h1.novel-title").text().trim() || "Untitled", + cover: loadedCheerio("figure.cover > img").attr("data-src"), + author: loadedCheerio(".author > a > span").text(), + summary: loadedCheerio(".summary > .content").text().trim(), + status: loadedCheerio(".header-stats span:last strong").text(), + totalPages: Math.ceil(totalChapters/100), chapters: [], }; - novel.name = loadedCheerio("h1.novel-title").text().trim(); + novel.genres = loadedCheerio('.categories ul li') + .map((a,ex) => loadedCheerio(ex).text().trim()) + .toArray() + .join(','); - novel.cover = loadedCheerio("figure.cover > img").attr("data-src"); - - novel.genres = ""; - - loadedCheerio(".categories > ul > li > a").each(function () { - novel.genres += loadedCheerio(this).text() + ","; - }); - - novel.genres = novel.genres.slice(0, -1); - - loadedCheerio("div.header-stats > span").each(function () { - if (loadedCheerio(this).find("small").text() === "Status") { - novel.status = loadedCheerio(this).find("strong").text(); - } - }); - - novel.author = loadedCheerio(".author > a > span").text(); - - novel.summary = loadedCheerio(".summary > .content").text().trim(); - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - await delay(1000); - const chapterListUrl = novelUrl + '/chapters?chorder=desc'; - - const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); - const loadedChapterList = parseHTML(chapterListBody); - - - const lastNumber = parseInt( - loadedChapterList('ul.chapter-list > li:nth-child(1)').attr('data-chapterno') ?? '0' - ); - const lastChapterUrl = loadedChapterList('ul.chapter-list > li:nth-child(1) > a').attr('href'); - - if(lastChapterUrl){ - const chapterUrlPrefix = this.baseUrl + lastChapterUrl.split('/').slice(1, -1).join('/'); - const chapters: Plugin.ChapterItem[] = []; - for(let i = 1; i <= lastNumber; i++){ - chapters.push({ - chapterNumber: i, - name: 'Chapter ' + i, - url: chapterUrlPrefix + '/chapter-' + i, - }) - } - novel.chapters = chapters; - } return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); + + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '/chapters/page-' + page; + const body = await fetchApi(url).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.chapter-list li').each(function () { + const chapterName = + 'Chapter ' + + loadedCheerio(this).find('.chapter-no').text().trim() + + ' - '; + loadedCheerio(this).find('.chapter-title').text().trim(); + + const releaseDate = loadedCheerio(this) + .find('.chapter-update') + .attr('datetime'); + + const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, + releaseTime: dayjs(releaseDate).toISOString(), + })}); + const chapters = chapter; + return {chapters}; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); const loadedCheerio = parseHTML(body); @@ -119,9 +120,10 @@ class LightNovelWorld implements Plugin.PluginBase { return chapterText; } + async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}lnsearchlive`; - const link = `${this.baseUrl}search`; + const url = `${this.site}lnsearchlive`; + const link = `${this.site}search`; const response = await fetchApi(link).then((r) => r.text()); const token = parseHTML(response); let verifytoken = token("#novelSearchForm > input").attr("value"); @@ -152,22 +154,93 @@ class LightNovelWorld implements Plugin.PluginBase { .text() .trim(); const novelCover = loadedCheerio(ele).find("img").attr("src"); - const novelUrl = - this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.substring(1); - + const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1); + if (!novelUrl) return; novels.push({ name: novelName, - url: novelUrl, + path: novelUrl, cover: novelCover, }); }); return novels; } + async fetchImage(url: string): Promise { return fetchFile(url, {headers: this.headers}); } + filters = { + order: { + value: "popular", + label: "Order by", + options: [ + { label: "New", value: "new" }, + { label: "Popular", value: "popular" }, + { label: "Updates", value: "updated" }, + ], + type: FilterTypes.Picker, + }, + status: { + value: "all", + label: "Status", + options: [ + { label: "All", value: "all" }, + { label: "Completed", value: "completed" }, + { label: "Ongoing", value: "ongoing" } + ], + type: FilterTypes.Picker, + }, + genres: { + value: "all", + label: "Genre", + options: [ + { label: "All", value: "all" }, + { label: "Action", value: "action" }, + { label: "Adventure", value: "adventure" }, + { label: "Drama", value: "drama" }, + { label: "Fantasy", value: "fantasy" }, + { label: "Harem", value: "harem" }, + { label: "Martial Arts", value: "martial-arts" }, + { label: "Mature", value: "mature" }, + { label: "Romance", value: "romance" }, + { label: "Tragedy", value: "tragedy" }, + { label: "Xuanhuan", value: "xuanhuan" }, + { label: "Ecchi", value: "ecchi" }, + { label: "Comedy", value: "comedy" }, + { label: "Slice of Life", value: "slice-of-life" }, + { label: "Mystery", value: "mystery" }, + { label: "Supernatural", value: "supernatural" }, + { label: "Psychological", value: "psychological" }, + { label: "Sci-fi", value: "sci-fi" }, + { label: "Xianxia", value: "xianxia" }, + { label: "School Life", value: "school-life" }, + { label: "Josei", value: "josei" }, + { label: "Wuxia", value: "wuxia" }, + { label: "Shounen", value: "shounen" }, + { label: "Horror", value: "horror" }, + { label: "Mecha", value: "mecha" }, + { label: "Historical", value: "historical" }, + { label: "Shoujo", value: "shoujo" }, + { label: "Adult", value: "adult" }, + { label: "Seinen", value: "seinen" }, + { label: "Sports", value: "sports" }, + { label: "Lolicon", value: "lolicon" }, + { label: "Gender Bender", value: "gender-bender" }, + { label: "Shounen Ai", value: "shounen-ai" }, + { label: "Yaoi", value: "yaoi" }, + { label: "Video Games", value: "video-games" }, + { label: "Smut", value: "smut" }, + { label: "Magical Realism", value: "magical-realism" }, + { label: "Eastern Fantasy", value: "eastern-fantasy" }, + { label: "Contemporary Romance", value: "contemporary-romance" }, + { label: "Fantasy Romance", value: "fantasy-romance" }, + { label: "Shoujo Ai", value: "shoujo-ai" }, + { label: "Yuri", value: "yuri" }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new LightNovelWorld(); diff --git a/plugins/english/lnmtl.broken.ts b/plugins/english/lnmtl.broken.ts deleted file mode 100644 index dc93e6d..0000000 --- a/plugins/english/lnmtl.broken.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Chapter, Novel, Plugin } from "@typings/plugin"; - -const pluginId = "lnmtl"; -const baseUrl = "https://lnmtl.com/"; - -export const popularNovels: Plugin.popularNovels = async function (page) { - let url = baseUrl + "novel?page=" + page; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let novels: Novel.Item[] = []; - - loadedCheerio(".media").each(function () { - const novelName = loadedCheerio(this).find("h4").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h4 > a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; -}; - -export const parseNovelAndChapters: Plugin.parseNovelAndChapters = - async function (novelUrl) { - const url = novelUrl; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - const novel: Novel.instance = { - url, - chapters: [], - }; - - novel.name = loadedCheerio(".novel-name").text(); - - novel.cover = loadedCheerio("div.novel").find("img").attr("src"); - - novel.summary = loadedCheerio("div.description").text().trim(); - - loadedCheerio(".panel-body > dl").each(function () { - let detailName = loadedCheerio(this).find("dt").text().trim(); - let detail = loadedCheerio(this).find("dd").text().trim(); - - switch (detailName) { - case "Authors": - novel.author = detail; - break; - case "Current status": - novel.status = detail; - break; - } - }); - - novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")') - .next() - .text() - .trim() - .replace(/\s\s/g, ","); - - let volumes = JSON.parse( - loadedCheerio("main") - .next() - .html() - .match(/lnmtl.volumes = \[(.*?)\]/).[0] - .replace("lnmtl.volumes = ", "") - ); - - let chapters: Chapter.Item[] = []; - - volumes = volumes.map((volume) => volume.id); - - for (const volume of volumes) { - let volumeData = await fetchApi( - `https://lnmtl.com/chapter?page=1&volumeId=${volume}` - ); - volumeData = await volumeData.json(); - - // volumeData = volumeData.data.map((volume) => volume.slug); - - for (let i = 1; i <= volumeData.last_page; i++) { - let chapterData = await fetchApi( - `https://lnmtl.com/chapter?page=${i}&volumeId=${volume}` - ); - chapterData = await chapterData.json(); - - chapterData = chapterData.data.map((chapter) => ({ - name: `#${chapter.number} ${chapter.title}`, - url: `${baseUrl}chapter/${chapter.slug}`, - releaseTime: chapter.created_at, - })); - - chapters.push(...chapterData); - } - } - - novel.chapters = chapters; - - return novel; - }; - -export const parseChapter: Plugin.parseChapter = async function (chapterUrl) { - const body = await fetchApi(chapterUrl, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - loadedCheerio(".original").replaceWith("

"); - - let chapterText = loadedCheerio(".chapter-body").html(); - - if (!chapterText) { - chapterText = loadedCheerio(".alert.alert-warning").text(); - } - - return chapterText; -}; - -export const searchNovels: Plugin.searchNovels = async function (searchTerm) { - const url = "https://lnmtl.com/term"; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let novels:Novel.Item[] = loadedCheerio("footer") - .next() - .next() - .html() - .match(/local: \[(.*?)\]/)[0] - .replace("local: ", ""); - - novels = JSON.parse(novels); - - novels = novels.filter((novel) => - novel.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - novels = novels.map((novel) => ({ - name: novel.name, - url: novel.slug, - cover: novel.image, - })); - - return novels; -}; - -export const fetchImage: Plugin.fetchImage = async function (url) { - const headers = { - Referer: baseUrl, - }; - return await fetchFile(url, { headers: headers }); -}; - -module.exports = { - id: pluginId, - name: "LNMTL", - version: "1.0.0", - icon: "src/en/lnmtl/icon.png", - site: baseUrl, - protected: true, - fetchImage, - popularNovels, - parseNovelAndChapters, - parseChapter, - searchNovels, -}; diff --git a/plugins/english/lnmtl.ts b/plugins/english/lnmtl.ts index 23451e1..15533ba 100644 --- a/plugins/english/lnmtl.ts +++ b/plugins/english/lnmtl.ts @@ -3,19 +3,22 @@ import { fetchApi, fetchFile } from "@libs/fetch"; import { FilterTypes, Filters } from "@libs/filterInputs"; import { Plugin } from "@typings/plugin"; -class LnMTLPlugin implements Plugin.PluginBase { +class LnMTLPlugin implements Plugin.PagePlugin { id = "lnmtl"; name = "LnMTL"; icon = "src/en/lnmtl/icon.png"; site = "https://lnmtl.com/"; version = "1.0.0"; + async sleep (ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) + } + async popularNovels( page: number, { filters }: Plugin.PopularNovelsOptions ): Promise { let link = this.site + "novel?"; - link += `orderBy=${filters.order.value}`; link += `&order=${filters.sort.value}`; link += `&filter=${filters.storyStatus.value}`; @@ -28,17 +31,17 @@ class LnMTLPlugin implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio(".media").each(function () { - const novelName = loadedCheerio(this).find("h4").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h4 > a").attr("href"); + loadedCheerio(".media").each((i,el) => { + const novelName = loadedCheerio(el).find("h4").text(); + const novelCover = loadedCheerio(el).find("img").attr("src"); + const novelUrl = loadedCheerio(el).find("h4 > a").attr("href"); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; novels.push(novel); @@ -47,24 +50,29 @@ class LnMTLPlugin implements Plugin.PluginBase { return novels; }; - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetchApi(url); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); const body = await result.text(); const loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel = { - url, + const volumes = JSON.parse( + loadedCheerio("main") + .next() + .html() + ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] + ?.replace("lnmtl.volumes = ", "") || "" + ); + + const novel: Plugin.SourceNovel & {totalPages: number} = { + path: novelPath, + name: loadedCheerio(".novel-name").text() || 'Untitled', + cover: loadedCheerio("div.novel").find("img").attr("src"), + summary: loadedCheerio("div.description").text().trim(), + totalPages: volumes.length, chapters: [], }; - novel.name = loadedCheerio(".novel-name").text(); - - novel.cover = loadedCheerio("div.novel").find("img").attr("src"); - - novel.summary = loadedCheerio("div.description").text().trim(); - loadedCheerio(".panel-body > dl").each(function () { const detailName = loadedCheerio(this).find("dt").text().trim(); const detail = loadedCheerio(this).find("dd").text().trim(); @@ -86,52 +94,66 @@ class LnMTLPlugin implements Plugin.PluginBase { .toArray() .join(","); - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - let volumes = JSON.parse( + return novel; + }; + + async parsePage(novelPath: string, page: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const volumes = JSON.parse( loadedCheerio("main") .next() .html() ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] ?.replace("lnmtl.volumes = ", "") || "" - ); + )[+page-1]; const chapter: Plugin.ChapterItem[] = []; - - volumes = volumes.map((volume: { id: number; }) => volume.id); - - for (const volume of volumes) { - const volumeData = await fetchApi( - `${this.site}chapter?page=1&volumeId=${volume}` - ); - const volumePage = await volumeData.json(); - - // volumeData = volumeData.data.map((volume) => volume.slug); - - for (let i = 1; i <= volumePage.last_page; i++) { - const chapterData = await fetchApi( - `${this.site}chapter?page=${i}&volumeId=${volume}` - ); - const chapterInfo = await chapterData.json(); - - const chapterDetails = chapterInfo.data.map( - (chapter: { number: number; title: string; slug: string; created_at: string; }) => ({ - name: `#${chapter.number} ${chapter.title}`, - url: `${this.site}chapter/${chapter.slug}`, - releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert - })); - - chapter.push(...chapterDetails); - } - await delay(1000); + interface ChapterEntry { + number: number; + title: string; + slug: string; + created_at: string; } - novel.chapters = chapter; + await this.sleep(1000); + const volumeData = await fetchApi( + `${this.site}chapter?volumeId=${volumes.id}` + ); + const volumePage = await volumeData.json(); + const firstPage = volumePage.data.map( + (chapter: ChapterEntry) => ({ + name: `#${chapter.number} - ${chapter.title}`, + path: `chapter/${chapter.slug}`, + releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert + })) + chapter.push(...firstPage); - return novel; - }; + for (let i = 2; i <= volumePage.last_page; i++) { + await this.sleep(1000); + const chapterData = await fetchApi( + `${this.site}chapter?page=${i}&volumeId=${volumes.id}` + ); + const chapterInfo = await chapterData.json(); - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); + const chapterDetails = chapterInfo.data.map( + (chapter: ChapterEntry) => ({ + name: `#${chapter.number} ${chapter.title}`, + path: `chapter/${chapter.slug}`, + releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert + })); + + chapter.push(...chapterDetails); + } + const chapters = chapter; + return {chapters}; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -171,11 +193,11 @@ class LnMTLPlugin implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = []; nov.map((res: { name: string; slug: string; image: string; }) => { const novelName = res.name; - const novelUrl = `${this.site}novel/${res.slug}`; + const novelUrl = `novel/${res.slug}`; const novelCover = res.image; const novel = { - url: novelUrl, + path: novelUrl, name: novelName, cover: novelCover, }; diff --git a/plugins/english/mtlnovel.ts b/plugins/english/mtlnovel.ts index 60ee5bd..ef04f4e 100644 --- a/plugins/english/mtlnovel.ts +++ b/plugins/english/mtlnovel.ts @@ -1,7 +1,7 @@ import { load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { Filters, FilterTypes } from "@libs/filterInputs"; class MTLNovel implements Plugin.PluginBase { id = "mtlnovel"; @@ -9,30 +9,38 @@ class MTLNovel implements Plugin.PluginBase { version = "1.0.0"; icon = "src/en/mtlnovel/icon.png"; site = "https://www.mtlnovel.com/"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}alltime-rank/page/${pageNo}`; - const body = await fetchApi(url).then((result) => result.text()); - console.log(body); + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + let link = `${this.site}novel-list/?`; + link += `orderby=${filters.order.value}`; + link += `&order=${filters.sort.value}`; + link += `&status=${filters.storyStatus.value}`; + link += `&pg=${page}` + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + const body = await fetch(link, {headers}).then((result) => result.text()); + const loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + const novels: Plugin.NovelItem[] = []; - loadedCheerio("div.box.wide").each(function () { - const novelName = loadedCheerio(this) + loadedCheerio("div.box.wide").each((i,el) => { + const novelName = loadedCheerio(el) .find("a.list-title") .text() .slice(4); - const novelCover = loadedCheerio(this).find("amp-img").attr("src"); - const novelUrl = loadedCheerio(this).find("a.list-title").attr("href"); + const novelCover = loadedCheerio(el).find("amp-img").attr("src"); + const novelUrl = loadedCheerio(el).find("a.list-title").attr("href"); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; novels.push(novel); @@ -40,65 +48,56 @@ class MTLNovel implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + async parseNovel(novelPath: string): Promise { + const headers = new Headers(); + headers.append('Referer', `${this.site}novel-list/`) + headers.append('Alt-Used', 'www.mtlnovel.com'); - let headers = { - Referer: `${this.baseUrl}alltime-rank/`, - }; - - const body = await fetchApi( - url, + const body = await fetch( + this.site + novelPath, { - method: "GET", - headers: headers, + headers }, ).then((result) => result.text()); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio("h1.entry-title").text() || 'Untitled', + cover: loadedCheerio(".nov-head > amp-img").attr("src"), + summary: loadedCheerio("div.desc > h2").next().text().trim(), + author: loadedCheerio("#author").text(), + status: loadedCheerio("#status").text(), + genres: loadedCheerio("#genre").text().replace(/\s+/g, ""), chapters: [], }; - novel.name = loadedCheerio("h1.entry-title").text(); + const chapterListUrl = this.site + novelPath + "chapter-list/"; - novel.cover = loadedCheerio(".nov-head > amp-img").attr("src"); - - novel.summary = loadedCheerio("div.desc > h2").next().text().trim(); - - novel.author = loadedCheerio("#author").text(); - - novel.status = loadedCheerio("#status").text(); - - novel.genres = loadedCheerio("#genre").text().replace(/\s+/g, ""); - - const chapterListUrl = url + "chapter-list/"; - - async function getChapters() { - const listResult = await fetch(chapterListUrl); + const getChapters = async () => { + const listResult = await fetch(chapterListUrl, {headers}); const listBody = await listResult.text(); loadedCheerio = parseHTML(listBody); - let chapter: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem[] = []; loadedCheerio("div.ch-list") .find("a.ch-link") - .each(function () { - const chapterName = loadedCheerio(this) + .each((i,el) => { + const chapterName = loadedCheerio(el) .text() .replace("~ ", ""); const releaseDate = null; - const chapterUrl = loadedCheerio(this).attr("href"); - if (chapterUrl) { - chapter.push({ - url: chapterUrl, - name: chapterName, - releaseTime: releaseDate, - }); - } + const chapterUrl = loadedCheerio(el).attr("href"); + if (!chapterUrl) return; + chapter.push({ + path: chapterUrl.replace(this.site, ''), + name: chapterName, + releaseTime: releaseDate, + }); + }); return chapter.reverse(); } @@ -107,8 +106,11 @@ class MTLNovel implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); + + async parseChapter(chapterPath: string): Promise { + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + const body = await fetch(this.site + chapterPath, {headers}).then((r) => r.text()); const loadedCheerio = parseHTML(body); @@ -116,17 +118,20 @@ class MTLNovel implements Plugin.PluginBase { return chapterText; } + async searchNovels(searchTerm: string, pageNo: number): Promise { + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); const searchUrl = - this.baseUrl + + this.site + "wp-admin/admin-ajax.php?action=autosuggest&q=" + searchTerm + "&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com"; - const res = await fetch(searchUrl); + const res = await fetch(searchUrl, {headers}); const result = await res.json(); - let novels: Plugin.NovelItem[] = []; + const novels: Plugin.NovelItem[] = []; interface SearchEntry { title: string; thumbnail: string; @@ -135,12 +140,9 @@ class MTLNovel implements Plugin.PluginBase { result.items[0].results.map((item: SearchEntry) => { const novelName = item.title.replace(/<\/?strong>/g, ""); const novelCover = item.thumbnail; - const novelUrl = item.permalink.replace( - "https://www.mtlnovel.com/", - "" - ); + const novelUrl = item.permalink.replace(this.site, ''); - const novel = { name: novelName, cover: novelCover, url: novelUrl }; + const novel = { name: novelName, cover: novelCover, path: novelUrl }; novels.push(novel); }); @@ -148,8 +150,42 @@ class MTLNovel implements Plugin.PluginBase { return novels; } async fetchImage(url: string): Promise { - return fetchFile(url); + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + return fetchFile(url, {headers}); } + filters = { + order: { + value: "view", + label: "Order by", + options: [ + { label: "Date", value: "date" }, + { label: "Name", value: "name" }, + { label: "Rating", value: "rating" }, + { label: "View", value: "view" }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: "desc", + label: "Sort by", + options: [ + { label: "Descending", value: "desc" }, + { label: "Ascending", value: "asc" }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + value: "all", + label: "Status", + options: [ + { label: "All", value: "all" }, + { label: "Ongoing", value: "ongoing" }, + { label: "Complete", value: "completed" }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new MTLNovel(); diff --git a/plugins/english/mtlreader.ts b/plugins/english/mtlreader.ts index 3124aed..163ce85 100644 --- a/plugins/english/mtlreader.ts +++ b/plugins/english/mtlreader.ts @@ -1,4 +1,4 @@ -import { load as parseHTML } from "cheerio"; +import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; import { Filters } from "@libs/filterInputs"; @@ -9,27 +9,20 @@ class MTLReader implements Plugin.PluginBase { version = "1.0.0"; icon = "src/en/mtlreader/icon.png"; site = "https://mtlreader.com/"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}novels?page=${pageNo}`; + parseNovels(loadedCheerio: CheerioAPI){ + const novels: Plugin.NovelItem[] = []; - const body = await fetchApi(url).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".col-md-4.col-sm-4").each(function () { - const novelName = loadedCheerio(this).find("h5").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h5 > a").attr("href"); + loadedCheerio(".col-md-4").each((i,el) => { + const novelName = loadedCheerio(el).find("h5").text(); + const novelCover = loadedCheerio(el).find("img").attr("src"); + const novelUrl = loadedCheerio(el).find("h5 > a").attr("href"); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; novels.push(novel); @@ -37,43 +30,46 @@ class MTLReader implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + + async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { + const url = `${this.site}novels?page=${pageNo}`; const body = await fetchApi(url).then((r) => r.text()); - let loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((r) => r.text()); + + const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio(".agent-title").text().trim() || "Untitled", + cover: loadedCheerio(".agent-p-img > img").attr("src"), + summary: loadedCheerio("#editdescription").text().trim(), chapters: [], }; - novel.name = loadedCheerio(".agent-title").text().trim(); - - novel.cover = loadedCheerio(".agent-p-img > img").attr("src"); - - novel.summary = loadedCheerio("#editdescription").text().trim(); - novel.author = loadedCheerio("i.fa-user") .parent() .text() .replace("Author: ", "") .trim(); - let chapter: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem[] = []; - loadedCheerio("tr.spaceUnder").each(function () { - const chapterName = loadedCheerio(this).find("a").text().trim(); - const releaseDate = null; - const chapterUrl = loadedCheerio(this).find("a").attr("href"); + loadedCheerio("tr.spaceUnder").each((i,el) => { + const chapterName = loadedCheerio(el).find("a").text().trim(); + const chapterUrl = loadedCheerio(el).find("a").attr("href"); if (!chapterUrl) return; chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl.replace(this.site, ''), }); }); @@ -81,8 +77,8 @@ class MTLReader implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); const loadedCheerio = parseHTML(body); @@ -92,32 +88,16 @@ class MTLReader implements Plugin.PluginBase { return chapterText; } async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}search?input=${searchTerm}`; + const body = await fetchApi(this.site).then((r) => r.text()); + const tokenCheerio = parseHTML(body); + const token = tokenCheerio('input[name="_token"]').attr('value'); - const body = await fetchApi(url).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".col-md-4.col-sm-4").each(function () { - const novelName = loadedCheerio(this).find("h5").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h5 > a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; + const searchUrl = `${this.site}search?_token=${token}&input=${searchTerm}`; + const seacrhBody = await fetchApi(searchUrl).then((r) => r.text()); + const loadedCheerio = parseHTML(seacrhBody); + return this.parseNovels(loadedCheerio); } + async fetchImage(url: string): Promise { return fetchFile(url); } diff --git a/plugins/english/nobleMTL.ts b/plugins/english/nobleMTL.ts deleted file mode 100644 index bb7c295..0000000 --- a/plugins/english/nobleMTL.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; - -class NobleMTL implements Plugin.PluginBase { - id = "noblemtl"; - name = "NobleMTL"; - icon = "src/en/noblemtl/icon.png"; - site = "https://noblemtl.com/"; - version = "1.0.0"; - - parseNovels(loadedCheerio: CheerioAPI) { - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.bs").each(function () { - const novelName = loadedCheerio(this).find(".ntitle").text().trim(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novelUrl = loadedCheerio(this).find("a").attr("href"); - - if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}series/?page=${pageNo}`; - - if (filters.genres.value.length) - link += filters.genres.value.map((i) => `&genre[]=${i}`).join(""); - - if (filters.type.value.length) - link += filters.type.value.map((i) => `&lang[]=${i}`).join(""); - - link += "&status=" + filters.status.value; - - link += "&order=" + filters.order.value; - - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio(".entry-title").text(); - - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - - loadedCheerio("div.spe > span").each(function () { - const detailName = loadedCheerio(this).find("b").text().trim(); - const detail = loadedCheerio(this) - .find("b") - .remove() - .end() - .text() - .trim(); - - switch (detailName) { - case "Author:": - novel.author = detail; - break; - case "Status:": - novel.status = detail; - break; - } - }); - - novel.genres = loadedCheerio(".genxed") - .text() - .trim() - .replace(/\s/g, ","); - - loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove(); - novel.summary = loadedCheerio('div[itemprop="description"]') - .find("br") - .replaceWith("\n") - .end() - .text(); - - let chapter: Plugin.ChapterItem[] = []; - - loadedCheerio(".eplister") - .find("li") - .each(function () { - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - if (!chapterUrl) return; - const chapterName = - loadedCheerio(this).find(".epl-num").text() + - " - " + - loadedCheerio(this).find(".epl-title").text(); - const releaseDate = loadedCheerio(this) - .find(".epl-date") - .text() - .trim(); // Feb 25, 2024 - - if (!releaseDate) return; - const months = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", - ]; - const rx = new RegExp( - `(${months.join("|")}).*? (\\d{2}).*?(\\d{4})`, - "i" - ).exec(releaseDate); - if (!rx) return; - const year = +rx[3]; - const month = months.indexOf(rx[1].toLowerCase()); - const day = +rx[2]; - - if (month < 0) return; - - chapter.push({ - name: chapterName, - releaseTime: new Date(year, month, day).toISOString(), - url: chapterUrl, - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio(".epcontent").html() || ""; - - return chapterText; - } - - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const url = `${this.site}page/${pageNo}/?s=${searchTerm}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - label: "Sort By", - options: [ - { label: "Default", value: "" }, - { label: "A-Z", value: "title" }, - { label: "Z-A", value: "titlereverse" }, - { label: "Latest Update", value: "update" }, - { label: "Latest Added", value: "latest" }, - { label: "Popular", value: "popular" }, - ], - type: FilterTypes.Picker, - value: "", - }, - - status: { - label: "Status", - options: [ - { label: "All", value: "" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Hiatus", value: "hiatus" }, - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - value: "", - }, - type: { - label: "Type", - options: [ - { label: "Chinese novel", value: "chinese-novel" }, - { label: "habyeol", value: "habyeol" }, - { label: "korean novel", value: "korean-novel" }, - { label: "Web Novel", value: "web-novel" }, - { label: "삼심", value: "%ec%82%bc%ec%8b%ac" }, - { label: "호곡", value: "%ed%98%b8%ea%b3%a1" }, - ], - type: FilterTypes.CheckboxGroup, - value: [], - }, - genres: { - label: "Genres", - options: [ - { label: "A.I", value: "a.i" }, - { label: "Academy", value: "academy" }, - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Alternative History", value: "alternative-history" }, - { label: "Another World", value: "another-world" }, - { label: "Apocalypse", value: "apocalypse" }, - { label: "Bromance", value: "bromance" }, - { label: "Comedy", value: "comedy" }, - { label: "Dark fantasy", value: "dark-fantasy" }, - { label: "Demons", value: "demons" }, - { label: "Drama", value: "drama" }, - { label: "Dystopia", value: "dystopia" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Entertainment", value: "entertainment" }, - { label: "Exhaustion", value: "exhaustion" }, - { label: "Fanfiction", value: "fanfiction" }, - { label: "fantasy", value: "fantasy" }, - { label: "finance", value: "finance" }, - { label: "Full color", value: "full-color" }, - { label: "Game", value: "game" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Genius", value: "genius" }, - { label: "Harem", value: "harem" }, - { label: "Hero", value: "hero" }, - { label: "Historical", value: "historical" }, - { label: "Hunter", value: "hunter" }, - { label: "korean novel", value: "korean-novel" }, - { label: "Light Novel", value: "light-novel" }, - { - label: "List Adventure Manga Genres", - value: "list-adventure-manga-genres", - }, - { label: "Long Strip", value: "long-strip" }, - { label: "Love comedy", value: "love-comedy" }, - { label: "magic", value: "magic" }, - { label: "Manhua", value: "manhua" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Medieval", value: "medieval" }, - { label: "Misunderstanding", value: "misunderstanding" }, - { label: "Modern", value: "modern" }, - { label: "modern fantasy", value: "modern-fantasy" }, - { label: "music", value: "music" }, - { label: "Mystery", value: "mystery" }, - { label: "Necromancy", value: "necromancy" }, - { label: "No Romance", value: "no-romance" }, - { label: "NTL", value: "ntl" }, - { label: "o", value: "o" }, - { label: "Obsession", value: "obsession" }, - { label: "Politics", value: "politics" }, - { label: "Possession", value: "possession" }, - { label: "Programming", value: "programming" }, - { label: "Psychological", value: "psychological" }, - { label: "Pure Love", value: "pure-love" }, - { label: "Redemption", value: "redemption" }, - { label: "Regression", value: "regression" }, - { label: "Regret", value: "regret" }, - { label: "Reincarnation", value: "reincarnation" }, - { label: "Revenge", value: "revenge" }, - { label: "Romance", value: "romance" }, - { label: "Romance Fanrasy", value: "romance-fanrasy" }, - { label: "Salvation", value: "salvation" }, - { label: "School Life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Science fiction", value: "science-fiction" }, - { label: "Seinen", value: "seinen" }, - { label: "Shounen", value: "shounen" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Soft yandere", value: "soft-yandere" }, - { label: "Sports", value: "sports" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Survival", value: "survival" }, - { label: "system", value: "system" }, - { label: "Time limit", value: "time-limit" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Transmigration", value: "transmigration" }, - { label: "TS", value: "ts" }, - { label: "Tsundere", value: "tsundere" }, - { label: "Unique", value: "unique" }, - { label: "Wholesome", value: "wholesome" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Yandere", value: "yandere" }, - { label: "Yuri", value: "yuri" }, - ], - type: FilterTypes.CheckboxGroup, - value: [], - }, - } satisfies Filters; -} - -export default new NobleMTL(); diff --git a/plugins/english/novelbin.ts b/plugins/english/novelbin.ts index 74afbc3..4c98249 100644 --- a/plugins/english/novelbin.ts +++ b/plugins/english/novelbin.ts @@ -1,29 +1,21 @@ -import { load as parseHTML } from "cheerio"; +import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { Filters, FilterTypes } from "@libs/filterInputs"; class NovelBin implements Plugin.PluginBase { id = "novelbin"; name = "Novel Bin"; icon = "src/en/novelbin/icon.png"; - site = "https://novelmax.net/"; - filters?: Filters | undefined; + site = "https://binnovel.com/"; version = "1.0.0"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}sort/p/?page=${pageNo}`; + parseNovels(loadedCheerio:CheerioAPI){ + const novels: Plugin.NovelItem[] = []; - const body = await fetchApi(url).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".col-novel-main .list-novel .row").each(function () { - const novelName = loadedCheerio(this).find("h3.novel-title > a").text(); - const novelCover = loadedCheerio(this).find("img.cover").attr("src"); - const novelUrl = loadedCheerio(this) + loadedCheerio(".col-novel-main .list-novel .row").each((i,el) => { + const novelName = loadedCheerio(el).find("h3.novel-title > a").text(); + const novelCover = loadedCheerio(el).find("img.cover").attr("src"); + const novelUrl = loadedCheerio(el) .find("h3.novel-title > a") .attr("href"); @@ -32,7 +24,7 @@ class NovelBin implements Plugin.PluginBase { const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; novels.push(novel); @@ -40,23 +32,38 @@ class NovelBin implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const body = await fetchApi(url).then((r) => r.text()); + + async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise { + let link = this.site; + if (filters.genre.value !== '') + link += `gen/${filters.genre.value}`; + else + link += `sort/${filters.sort.value}`; + + if (filters.complete.value) + link += `/completed`; + + link += `?page=${page}`; + + const body = await fetchApi(link).then((r) => r.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((r) => r.text()); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio("div.book > img").attr("alt") || "Untitled", + cover: loadedCheerio("div.book > img").attr("src"), + summary: loadedCheerio("div.desc-text").text().trim(), chapters: [], }; - novel.name = loadedCheerio("div.book > img").attr("alt"); - - novel.cover = loadedCheerio("div.book > img").attr("src"); - - novel.summary = loadedCheerio("div.desc-text").text().trim(); - loadedCheerio("ul.info > li > h3").each(function () { let detailName = loadedCheerio(this).text(); let detail = loadedCheerio(this) @@ -82,26 +89,24 @@ class NovelBin implements Plugin.PluginBase { const getChapters = async (id: string) => { const chapterListUrl = - this.baseUrl + "ajax/chapter-archive?novelId=" + id; + this.site + "ajax/chapter-archive?novelId=" + id; const data = await fetchApi(chapterListUrl); const chapterdata = await data.text(); loadedCheerio = parseHTML(chapterdata); - let chapter: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem[] = []; - loadedCheerio("ul.list-chapter > li").each(function () { - const chapterName = loadedCheerio(this).find("a").attr("title"); - const releaseDate = null; - const chapterUrl = loadedCheerio(this).find("a").attr("href"); + loadedCheerio("ul.list-chapter > li").each((i,el) => { + const chapterName = loadedCheerio(el).find("a").attr("title"); + const chapterUrl = loadedCheerio(el).find("a").attr("href"); if (!chapterName || !chapterUrl) return; chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl.replace(this.site, ''), }); }); return chapter; @@ -113,50 +118,114 @@ class NovelBin implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); const loadedCheerio = parseHTML(body); loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove(); - let chapterText = loadedCheerio("#chr-content").html() || ''; + const chapterText = loadedCheerio("#chr-content").html() || ''; return chapterText; } + async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}search/?keyword=${searchTerm}`; + const url = `${this.site}search/?keyword=${searchTerm}`; const body = await fetchApi(url).then((r) => r.text()); const loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio("div.col-novel-main > div.list-novel > .row").each( - function () { - const novelUrl = loadedCheerio(this) - .find("h3.novel-title > a") - .attr("href"); - - const novelName = loadedCheerio(this) - .find("h3.novel-title > a") - .text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - - if (novelUrl) { - novels.push({ - url: novelUrl, - name: novelName, - cover: novelCover, - }); - } - } - ); - - return novels; + return this.parseNovels(loadedCheerio); } + async fetchImage(url: string): Promise { return fetchFile(url); } + filters = { + sort: { + value: "popular-novels", + label: "Sort by", + options: [ + { label: "Latest Release", value: "update" }, + { label: "Hot Novel", value: "hot-novels" }, + { label: "Completed Novel", value: "c" }, + { label: "Most Popular", value: "popular-novels" }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: "", + label: "Genres", + options: [ + { label: "None", value: "" }, + { label: "Action", value: "action" }, + { label: "Adult", value: "adult" }, + { label: "Adventure", value: "adventure" }, + { label: "Anime", value: "anime" }, + { label: "Arts", value: "arts" }, + { label: "Comedy", value: "comedy" }, + { label: "Drama", value: "drama" }, + { label: "Eastern", value: "eastern" }, + { label: "Ecchi", value: "ecchi" }, + { label: "Fan-fiction", value: "fan-fiction" }, + { label: "Fanfiction", value: "fanfiction" }, + { label: "Fantas", value: "fantas" }, + { label: "Fantasy", value: "fantasy" }, + { label: "Game", value: "game" }, + { label: "Gender bender", value: "gender-bender" }, + { label: "Harem", value: "harem" }, + { label: "Hentai", value: "hentai" }, + { label: "Historical", value: "historical" }, + { label: "Horror", value: "horror" }, + { label: "Isekai", value: "isekai" }, + { label: "Josei", value: "josei" }, + { label: "Lgbt+", value: "lgbt+" }, + { label: "Litrpg", value: "litrpg" }, + { label: "Magic", value: "magic" }, + { label: "Magical realism", value: "magical-realism" }, + { label: "Manhua", value: "manhua" }, + { label: "Martial arts", value: "martial-arts" }, + { label: "Mature", value: "mature" }, + { label: "Mecha", value: "mecha" }, + { label: "Military", value: "military" }, + { label: "Modern life", value: "modern-life" }, + { label: "Movies", value: "movies" }, + { label: "Mystery", value: "mystery" }, + { label: "Portal fantasy", value: "portal-fantasy" }, + { label: "Psychological", value: "psychological" }, + { label: "Realistic fiction", value: "realistic-fiction" }, + { label: "Reincarnation", value: "reincarnation" }, + { label: "Romance", value: "romance" }, + { label: "School life", value: "school-life" }, + { label: "Sci-fi", value: "sci-fi" }, + { label: "Seinen", value: "seinen" }, + { label: "Shoujo", value: "shoujo" }, + { label: "Shoujo ai", value: "shoujo-ai" }, + { label: "Shounen", value: "shounen" }, + { label: "Shounen ai", value: "shounen-ai" }, + { label: "Slice of life", value: "slice-of-life" }, + { label: "Smut", value: "smut" }, + { label: "Sports", value: "sports" }, + { label: "Supernatural", value: "supernatural" }, + { label: "System", value: "system" }, + { label: "Tragedy", value: "tragedy" }, + { label: "Urban life", value: "urban-life" }, + { label: "Video games", value: "video-games" }, + { label: "Wuxia", value: "wuxia" }, + { label: "Xianxia", value: "xianxia" }, + { label: "Xuanhuan", value: "xuanhuan" }, + { label: "Yaoi", value: "yaoi" }, + { label: "Yuri", value: "yuri" } + ], + type: FilterTypes.Picker, + }, + complete: { + label: "Show Completed Novels Only", + value: false, + type: FilterTypes.Switch, + }, + } satisfies Filters; } + export default new NovelBin(); diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts new file mode 100644 index 0000000..def3fe4 --- /dev/null +++ b/plugins/english/novelbuddy.ts @@ -0,0 +1,171 @@ +import { CheerioAPI, load as parseHTML } from "cheerio"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { Plugin } from "@typings/plugin"; +import { Filters } from "@libs/filterInputs"; + +class NovelBuddy implements Plugin.PluginBase { + id = "novelbuddy"; + name = "NovelBuddy.io"; + site = "https://novelbuddy.io/"; + version = "1.0.0"; + icon = "src/en/novelbuddy/icon.png"; + filters?: undefined; //TODO: Filters, Filters in Filters required + + parseNovels(loadedCheerio: CheerioAPI){ + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(".book-item").each((idx, ele) => { + const novelName = loadedCheerio(ele).find(".title").text(); + const novelCover = + "https:" + loadedCheerio(ele).find("img").attr("data-src"); + const novelUrl = loadedCheerio(ele).find(".title a").attr("href")?.substring(1); + + if (!novelUrl) return; + + const novel = { name: novelName, cover: novelCover, path: novelUrl }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions + ): Promise { + const url = `${this.site}popular?page=${pageNo}`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio(".name h1").text().trim() || "Untitled", + cover: "https:" + loadedCheerio(".img-cover img").attr("data-src"), + summary: loadedCheerio('.section-body.summary').text().trim(), + chapters: [], + }; + + loadedCheerio('.meta.box p').each((i,el) => { + const detailName = loadedCheerio(el).find('strong').text(); + const detail = loadedCheerio(el).find('a'); + + switch(detailName){ + case "Authors :": + novel.author = detail.find('span') + .map((a,ex) => loadedCheerio(ex).text()) + .toArray() + .join(', '); + break; + case "Status :": + novel.status = detail.text(); + break; + case "Genres :": + novel.genres = detail.text().trim(); + break; + } + }) + const novelId = loadedCheerio('script') + .text() + .match(/bookId = (\d+);/)![1]; + const chapter: Plugin.ChapterItem[] = []; + + const getChapters = async (id: string) => { + const chapterListUrl = `${this.site}api/manga/${id}/chapters?source=detail`; + const data = await fetchApi(chapterListUrl); + const chapterlist = await data.text(); + console.log(chapterlist); + loadedCheerio = parseHTML(chapterlist); + + loadedCheerio("li").each((i,el) => { + const chapterName = loadedCheerio(el) + .find(".chapter-title") + .text() + .trim(); + + const releaseDate = loadedCheerio(el) + .find(".chapter-update") + .text() + .trim(); + + const months = [ + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", + ].join("|"); + const rx = new RegExp(`(${months}) (\\d{1,2}), (\\d{4})`,'i') + .exec(releaseDate); + if (!rx) return; + const year = +rx[3]; + const month = months.indexOf(rx[1].toLowerCase()); + const day = +rx[2]; + + const chapterUrl = loadedCheerio(el).find("a").attr("href")?.slice(1); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + releaseTime: new Date(year, month, day).toISOString(), + path: chapterUrl, + }); + }); + return chapter; + } + + novel.chapters = (await getChapters(novelId)).reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + loadedCheerio("#listen-chapter").remove(); + loadedCheerio("#google_translate_element").remove(); + + const chapterText = loadedCheerio(".chapter__content").html() || ''; + + return chapterText; + } + + async searchNovels(searchTerm: string, page: number): Promise { + const url = `${this.site}search?q=${searchTerm}&page=${page}`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async fetchImage(url: string): Promise { + return fetchFile(url); + } + +} + +export default new NovelBuddy(); diff --git a/plugins/english/novelforest.broken.ts b/plugins/english/novelforest.broken.ts deleted file mode 100644 index 4629ec0..0000000 --- a/plugins/english/novelforest.broken.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Chapter, Novel, Plugin } from "@typings/plugin"; -import { load as parseHTML } from "cheerio"; - -export const id = "novelforest"; -export const name = "Novel Forest (Broken)"; -export const version = "1.0.0"; -export const icon = "src/en/novelforest/icon.png"; -export const site = "https://novelforest.com/"; - - -const pluginId = id; -const baseUrl = site; - -export const popularNovels: Plugin.popularNovels = async function (page) { - const url = `${baseUrl}popular?page=${page}`; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let novels: Novel.Item[] = []; - - loadedCheerio(".book-item").each(function () { - const novelName = loadedCheerio(this).find(".title").text(); - const novelCover = - "https:" + loadedCheerio(this).find("img").attr("data-src"); - const novelUrl = - baseUrl + - loadedCheerio(this).find(".title a").attr("href")?.substring(1); - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; -}; - -export const parseNovelAndChapters: Plugin.parseNovelAndChapters = async ( - novelUrl -) => { - const url = novelUrl; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - const novel: Novel.instance = { - url, - chapters: [], - }; - - novel.name = loadedCheerio(".name h1").text().trim(); - - novel.cover = "https:" + loadedCheerio(".img-cover img").attr("data-src"); - - novel.summary = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content" - ) - .text() - .trim(); - - novel.author = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span" - ) - .text() - ?.trim(); - - novel.status = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2) > a > span" - ) - .text() - ?.trim(); - - novel.genres = loadedCheerio( - "body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(3)" - ) - .text() - ?.replace("Genres :", "") - .replace(/[\s\n]+/g, " ") - .trim(); - - let chapter: Chapter.Item[] = []; - - const chaptersUrl = - novelUrl.replace(baseUrl, "https://novelforest.com/api/novels/") + - "/chapters?source=detail"; - - const chaptersRequest = await fetch(chaptersUrl); - const chaptersHtml = await chaptersRequest.text(); - - loadedCheerio = parseHTML(chaptersHtml); - - loadedCheerio("li").each(function () { - const chapterName = loadedCheerio(this) - .find(".chapter-title") - .text() - .trim(); - - const releaseDate = loadedCheerio(this) - .find(".chapter-update") - .text() - .trim(); - - const chapterUrl = - baseUrl + loadedCheerio(this).find("a").attr("href")?.substring(1); - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapter; - - return novel; -}; - -export const parseChapter: Plugin.parseChapter = async function (chapterUrl) { - const body = await fetchApi(chapterUrl, {}, pluginId).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - loadedCheerio("#listen-chapter").remove(); - loadedCheerio("#google_translate_element").remove(); - - const chapterText = loadedCheerio(".chapter__content").html(); - - return chapterText; -}; - -export const searchNovels: Plugin.searchNovels = async function (searchTerm) { - const url = `${baseUrl}search?q=${searchTerm}`; - - const body = await fetchApi(url, {}, pluginId).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - let novels: Novel.Item[] = []; - - loadedCheerio(".book-item").each(function () { - const novelName = loadedCheerio(this).find(".title").text(); - const novelCover = - "https:" + loadedCheerio(this).find("img").attr("data-src"); - const novelUrl = - baseUrl + - loadedCheerio(this).find(".title a").attr("href")?.substring(1); - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; -}; - -export const fetchImage: Plugin.fetchImage = fetchFile; diff --git a/plugins/english/novelfull.ts b/plugins/english/novelfull.ts index f03b03d..e0acfe6 100644 --- a/plugins/english/novelfull.ts +++ b/plugins/english/novelfull.ts @@ -1,7 +1,7 @@ -import { load as parseHTML } from "cheerio"; +import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { Filters, FilterTypes } from "@libs/filterInputs"; class NovelFull implements Plugin.PluginBase { @@ -10,16 +10,9 @@ class NovelFull implements Plugin.PluginBase { version = "1.0.0"; icon = "src/en/novelfull/icon.png"; site = "https://novelfull.com/"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}most-popular?page=${pageNo}`; - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio:CheerioAPI){ + const novels: Plugin.NovelItem[] = []; loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => { const novelName = loadedCheerio(ele) @@ -27,19 +20,19 @@ class NovelFull implements Plugin.PluginBase { .text(); const novelCover = - this.baseUrl + loadedCheerio(ele).find("img").attr("src")?.slice(1); + this.site + loadedCheerio(ele).find("img").attr("src")?.slice(1); - const novelUrl = - this.baseUrl + - loadedCheerio(ele) - .find("h3.truyen-title > a") - .attr("href") - ?.slice(1); + const novelUrl = loadedCheerio(ele) + .find("h3.truyen-title > a") + .attr("href") + ?.slice(1); + + if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl, }; novels.push(novel); @@ -47,25 +40,37 @@ class NovelFull implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetchApi(url); + async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise { + let link = this.site; + if (filters.genre.value !== '') + link += `genre/${filters.genre.value}`; + else + link += filters.sort.value + + link += `?page=${page}`; + + const body = await fetchApi(link).then((r) => r.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio("div.book > img").attr("alt") || "Untitled", + cover: this.site + loadedCheerio("div.book > img").attr("src"), + summary: loadedCheerio("div.desc-text").text().trim(), + status: loadedCheerio('h3:contains("Status")').next().text(), chapters: [], }; - novel.name = loadedCheerio("div.book > img").attr("alt"); - - novel.cover = this.baseUrl + loadedCheerio("div.book > img").attr("src"); - - novel.summary = loadedCheerio("div.desc-text").text().trim(); - novel.author = loadedCheerio('h3:contains("Author")') .parent() .contents() @@ -78,91 +83,118 @@ class NovelFull implements Plugin.PluginBase { .toArray() .join(","); - novel.status = loadedCheerio('h3:contains("Status")').next().text(); - const novelId = loadedCheerio("#rating").attr("data-novel-id")!; + const chapter: Plugin.ChapterItem[] = []; - async function getChapters(id: string, baseUrl: string) { - const chapterListUrl = - baseUrl + "ajax/chapter-option?novelId=" + id; + const getChapters = async (id: string) => { + const chapterListUrl = this.site + "ajax/chapter-option?novelId=" + id; const data = await fetchApi(chapterListUrl); const chapterlist = await data.text(); loadedCheerio = parseHTML(chapterlist); - let chapter: Plugin.ChapterItem[] = []; - loadedCheerio("select > option").each(function () { const chapterName = loadedCheerio(this).text(); - const releaseDate = null; - const chapterUrl = - baseUrl + loadedCheerio(this).attr("value")?.slice(1); + const chapterUrl = loadedCheerio(this).attr("value")?.slice(1); + if (!chapterUrl) return; chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl, }); }); return chapter; } - novel.chapters = await getChapters(novelId, this.baseUrl); + novel.chapters = await getChapters(novelId); return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); loadedCheerio("#chapter-content div.ads").remove(); - let chapterText = loadedCheerio("#chapter-content").html() || ''; + const chapterText = loadedCheerio("#chapter-content").html() || ''; return chapterText; } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const searchUrl = `${this.baseUrl}search?keyword=${searchTerm}`; + + async searchNovels(searchTerm: string, page: number): Promise { + const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`; const result = await fetchApi(searchUrl); const body = await result.text(); const loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h3.truyen-title > a") - .text(); - - const novelCover = - this.baseUrl + loadedCheerio(ele).find("img").attr("src")?.slice(1); - - const novelUrl = - this.baseUrl + - loadedCheerio(ele) - .find("h3.truyen-title > a") - .attr("href") - ?.slice(1); - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; + return this.parseNovels(loadedCheerio); } + async fetchImage(url: string): Promise { return fetchFile(url); } + filters = { + sort: { + value: "most-popular", + label: "Sort by", + options: [ + { label: "Latest Release", value: "latest-release-novel" }, + { label: "Hot Novel", value: "hot-novel" }, + { label: "Completed Novel", value: "completed-novel" }, + { label: "Most Popular", value: "most-popular" }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: "", + label: "Genres", + options: [ + { label: "None", value: "" }, + { label: "Shounen", value: "Shounen" }, + { label: "Harem", value: "Harem" }, + { label: "Comedy", value: "Comedy" }, + { label: "Martial Arts", value: "Martial+Arts" }, + { label: "School Life", value: "School+Life" }, + { label: "Mystery", value: "Mystery" }, + { label: "Shoujo", value: "Shoujo" }, + { label: "Romance", value: "Romance" }, + { label: "Sci-fi", value: "Sci-fi" }, + { label: "Gender Bender", value: "Gender+Bender" }, + { label: "Mature", value: "Mature" }, + { label: "Fantasy", value: "Fantasy" }, + { label: "Horror", value: "Horror" }, + { label: "Drama", value: "Drama" }, + { label: "Tragedy", value: "Tragedy" }, + { label: "Supernatural", value: "Supernatural" }, + { label: "Ecchi", value: "Ecchi" }, + { label: "Xuanhuan", value: "Xuanhuan" }, + { label: "Adventure", value: "Adventure" }, + { label: "Action", value: "Action" }, + { label: "Psychological", value: "Psychological" }, + { label: "Xianxia", value: "Xianxia" }, + { label: "Wuxia", value: "Wuxia" }, + { label: "Historical", value: "Historical" }, + { label: "Slice of Life", value: "Slice+of+Life" }, + { label: "Seinen", value: "Seinen" }, + { label: "Lolicon", value: "Lolicon" }, + { label: "Adult", value: "Adult" }, + { label: "Josei", value: "Josei" }, + { label: "Sports", value: "Sports" }, + { label: "Smut", value: "Smut" }, + { label: "Mecha", value: "Mecha" }, + { label: "Yaoi", value: "Yaoi" }, + { label: "Shounen Ai", value: "Shounen+Ai" }, + { label: "History", value: "History" }, + { label: "Martial", value: "Martial" } + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new NovelFull(); diff --git a/plugins/english/novelhall.ts b/plugins/english/novelhall.ts index 876a372..3d75180 100644 --- a/plugins/english/novelhall.ts +++ b/plugins/english/novelhall.ts @@ -1,34 +1,35 @@ -import { load as parseHTML } from "cheerio"; +import { load, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; import { Filters } from "@libs/filterInputs"; +import { defaultCover } from "@libs/defaultCover"; class NovelHall implements Plugin.PluginBase { id = "novelhall"; name = "Novel Hall"; version = "1.0.0"; icon = "src/en/novelhall/icon.png"; - filters?: Filters | undefined; + filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect site = "https://novelhall.com/"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}all2022-${pageNo}.html`; + async popularNovels(page: number, options: Plugin.PopularNovelsOptions): Promise { + const url = `${this.site}all2022-${page}.html`; const body = await fetchApi(url).then((r) => r.text()); const loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + const novels: Plugin.NovelItem[] = []; loadedCheerio("li.btm").each((idx, ele) => { const novelName = loadedCheerio(ele).text().trim() - const novelUrl = - this.baseUrl + loadedCheerio(ele).find("a").attr("href"); + const novelUrl = loadedCheerio(ele).find("a").attr("href"); + if(!novelUrl) return; const novel = { name: novelName, - url: novelUrl, + cover: defaultCover, + path: novelUrl, }; novels.push(novel); @@ -36,77 +37,66 @@ class NovelHall implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((r) => r.text()); - const body = await fetchApi(url).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio(".book-info > h1").text() || 'Untitled', + cover: loadedCheerio('meta[property="og:image"]').attr('content'), + summary: loadedCheerio(".intro").text().trim(), chapters: [], }; - novel.name = loadedCheerio(".booknav2 > h1").text(); - - novel.cover = loadedCheerio('meta[property="og:image"]').attr( - "content" - ); - - novel.summary = loadedCheerio(".navtxt").text().trim(); - - novel.author = loadedCheerio('p:contains("Author")') + loadedCheerio('.total').find('p').remove(); + novel.author = loadedCheerio('.total span:contains("Author")') .text() .replace("Author:", "") .trim(); - novel.status = loadedCheerio('p:contains("Status")') + novel.status = loadedCheerio('.total span:contains("Status")') .text() .replace("Status:", "") .replace("Active", "Ongoing") .trim(); - novel.genres = loadedCheerio('p:contains("Genre")') - .text() - ?.replace("Genre:", "") - .trim(); + novel.genres = loadedCheerio('.total a') + .map((a,ex) => loadedCheerio(ex).text()) + .toArray() + .join(','); - let chapter: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem[] = []; loadedCheerio("#morelist ul > li").each((idx, ele) => { const chapterName = loadedCheerio(ele).find("a").text().trim(); - const releaseDate = null; - const chapterUrl = - this.baseUrl + loadedCheerio(ele).find("a").attr("href"); + const chapterUrl = loadedCheerio(ele).find("a").attr("href"); + if (!chapterUrl) return; chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl, }); }); novel.chapters = chapter; return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); + const loadedCheerio = parseHTML(body); const chapterText = loadedCheerio(".content").html() || ''; - return chapterText; } + async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}index.php?s=so&module=book&keyword=${searchTerm}`; - + const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`; const body = await fetchApi(url).then((r) => r.text()); + const loadedCheerio = parseHTML(body); - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; + const novels: Plugin.NovelItem[] = []; loadedCheerio("#article_list_content > li").each((idx, ele) => { const novelName = loadedCheerio(ele) @@ -115,13 +105,13 @@ class NovelHall implements Plugin.PluginBase { .replace(/\t+/g, "") .replace(/\n/g, " "); const novelCover = loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = - this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.slice(1); + const novelUrl = loadedCheerio(ele).find("a").attr("href")?.slice(1); + if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl, }; novels.push(novel); diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index d20d026..88ba4fa 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -25,7 +25,7 @@ class NovelUpdates implements Plugin.PluginBase { const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), }; novels.push(novel); @@ -40,7 +40,7 @@ class NovelUpdates implements Plugin.PluginBase { filters.novelType.value.length || filters.genres.value.include?.length || filters.genres.value.exclude?.length || - filters.storyStatus.value.length ){ + filters.storyStatus.value !== '' ){ link += "series-finder/?sf=1" } else if (showLatestNovels){ link += "latest-series/?st=1" @@ -72,8 +72,7 @@ class NovelUpdates implements Plugin.PluginBase { link += '&pg=' + page; - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => + const body = await fetchApi(link).then((result) => result.text() ); @@ -81,24 +80,21 @@ class NovelUpdates implements Plugin.PluginBase { return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; const result = await fetchApi(url); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio(".seriestitlenu").text() || "Untitled", + cover: loadedCheerio(".seriesimg > img").attr("src"), + author: loadedCheerio("#showauthors").text().trim(), chapters: [], }; - novel.name = loadedCheerio(".seriestitlenu").text(); - - novel.cover = loadedCheerio(".seriesimg > img").attr("src"); - - novel.author = loadedCheerio("#showauthors").text().trim(); - novel.genres = loadedCheerio("#seriesgenre") .children("a") .map((i, el) => loadedCheerio(el).text()) @@ -109,22 +105,22 @@ class NovelUpdates implements Plugin.PluginBase { ? "Ongoing" : "Completed"; - let type = loadedCheerio("#showtype").text().trim(); + const type = loadedCheerio("#showtype").text().trim(); - let summary = loadedCheerio("#editdescription").text().trim(); + const summary = loadedCheerio("#editdescription").text().trim(); novel.summary = summary + `\n\nType: ${type}`; - let chapter: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem[] = []; const novelId = loadedCheerio("input#mypostid").attr("value")!; - let formData = new FormData(); + const formData = new FormData(); formData.append("action", "nd_getchapters"); formData.append("mygrr", "0"); formData.append("mypostid", novelId); - let link = `${this.site}wp-admin/admin-ajax.php`; + const link = `${this.site}wp-admin/admin-ajax.php`; const text = await fetchApi( link, @@ -136,17 +132,15 @@ class NovelUpdates implements Plugin.PluginBase { loadedCheerio = parseHTML(text); - loadedCheerio("li.sp_li_chp").each(function () { - const chapterName = loadedCheerio(this).text().trim(); - const releaseDate = null; + loadedCheerio("li.sp_li_chp").each((i,el) => { + const chapterName = loadedCheerio(el).text().trim(); const chapterUrl = "https:" + - loadedCheerio(this).find("a").first().next().attr("href"); + loadedCheerio(el).find("a").first().next().attr("href"); chapter.push({ name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, + path: chapterUrl.replace(this.site, ''), }); }); @@ -154,6 +148,7 @@ class NovelUpdates implements Plugin.PluginBase { return novel; } + getLocation(href: string) { var match = href.match( /^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/ @@ -161,11 +156,11 @@ class NovelUpdates implements Plugin.PluginBase { return match && `${match[1]}//${match[3]}`; } - async parseChapter(chapterUrl: string): Promise { + async parseChapter(chapterPath: string): Promise { let chapterText = ""; const result = await fetchApi( - chapterUrl, + this.site + chapterPath, ); const body = await result.text(); @@ -175,29 +170,29 @@ class NovelUpdates implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); - let isWuxiaWorld = chapterUrl.toLowerCase().includes("wuxiaworld"); + let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld"); - let isBlogspot = chapterUrl.toLowerCase().includes("blogspot"); + let isBlogspot = result.url.toLowerCase().includes("blogspot"); - let isTumblr = chapterUrl.toLowerCase().includes("tumblr"); + let isTumblr = result.url.toLowerCase().includes("tumblr"); - let isWattpad = chapterUrl.toLowerCase().includes("wattpad"); + let isWattpad = result.url.toLowerCase().includes("wattpad"); - let isAnomalously = chapterUrl.toLowerCase().includes('anotivereads'); + let isAnomalously = result.url.toLowerCase().includes('anotivereads'); - let isBlossomTranslation = chapterUrl + let isBlossomTranslation = result.url .toLowerCase() .includes('blossomtranslation'); - let isLightNovelsTls = chapterUrl + let isLightNovelsTls = result.url .toLowerCase() .includes("lightnovelstranslations"); - let isiNovelTranslation = chapterUrl + let isiNovelTranslation = result.url .toLowerCase() .includes("inoveltranslation"); - let isTravisTranslation = chapterUrl + let isTravisTranslation = result.url .toLowerCase() .includes("travistranslations"); @@ -220,13 +215,13 @@ class NovelUpdates implements Plugin.PluginBase { .includes("wordpress"); } - let isRainOfSnow = chapterUrl.toLowerCase().includes("rainofsnow"); + let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow"); - let isWebNovel = chapterUrl.toLowerCase().includes("webnovel"); + let isWebNovel = result.url.toLowerCase().includes("webnovel"); - let isHostedNovel = chapterUrl.toLowerCase().includes("hostednovel"); + let isHostedNovel = result.url.toLowerCase().includes("hostednovel"); - let isScribbleHub = chapterUrl.toLowerCase().includes("scribblehub"); + let isScribbleHub = result.url.toLowerCase().includes("scribblehub"); if (isWuxiaWorld) { chapterText = loadedCheerio("#chapter-content").html()!; @@ -254,7 +249,7 @@ class NovelUpdates implements Plugin.PluginBase { } else if (isAnomalously) { chapterText = loadedCheerio('#comic').html()!; } else if (isiNovelTranslation) { - const link = 'https://api.' + chapterUrl.slice(8); + const link = 'https://api.' + result.url.slice(8); const json = await fetchApi(link).then(r => r.json()); chapterText = json.content.replace(/\n/g, '
'); if (json.notes) { @@ -317,12 +312,13 @@ class NovelUpdates implements Plugin.PluginBase { */ chapterText = chapterText.replace( /href="\//g, - `href="${this.getLocation(chapterUrl)}/` + `href="${this.getLocation(result.url)}/` ); } return chapterText; } + async searchNovels(searchTerm: string, page: number): Promise { const url =`${this.site}page/${page}/?s=${searchTerm}&post_type=seriesplans`; const result = await fetchApi(url); @@ -376,7 +372,6 @@ class NovelUpdates implements Plugin.PluginBase { label: "Language", value: [], options: [ - { label: "None", value: "" }, { label: "Chinese", value: "495" }, { label: "Filipino", value: "9181" }, { label: "Indonesian", value: "9179" }, diff --git a/plugins/english/pandaMTL.ts b/plugins/english/pandaMTL.ts deleted file mode 100644 index a424d2f..0000000 --- a/plugins/english/pandaMTL.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; - -class PandaMTL implements Plugin.PluginBase { - id = "pandamtl"; - name = "PandaMTL"; - icon = "src/en/wordpress/icon.png"; - site = "https://pandamtl.com/"; - version = "1.0.0"; - - - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}series/?page=${pageNo}`; - - if (filters.genres.value.length) - link += filters.genres.value.map((i) => `&genre[]=${i}`).join(""); - - if (filters.type.value.length) - link += filters.type.value.map((i) => `&lang[]=${i}`).join(""); - - link += "&status=" + filters.status.value; - link += "&order=" + filters.order.value; - - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.maindet").each(function () { - const novelName = loadedCheerio(this).find("h2").text(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novelUrl = loadedCheerio(this).find("h2 a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio("h1.entry-title").text(); - - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - - loadedCheerio(".serl:nth-child(3) > span").each(function () { - const detailName = loadedCheerio(this).text().trim(); - const detail = loadedCheerio(this).next().text().trim(); - - switch (detailName) { - case "Author": - novel.author = detail; - break; - } - }); - - novel.status = loadedCheerio(".sertostat > span").attr("class"); - - novel.genres = loadedCheerio(".sertogenre") - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - novel.summary = loadedCheerio(".sersys") - .find("br") - .replaceWith("\n") - .end() - .text(); - - let chapter: Plugin.ChapterItem[] = []; - - loadedCheerio(".eplister") - .find("li") - .each(function () { - const chapterName = - loadedCheerio(this).find(".epl-num").text() + - " - " + - loadedCheerio(this).find(".epl-title").text(); - - const releaseDate = loadedCheerio(this) - .find(".epl-date") - .text() - .trim(); - - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio(".epcontent").html() || ""; - - return chapterText; - } - async searchNovels( - searchTerm: string, - pageNo?: number | undefined - ): Promise { - const url = `${this.site}?s=${searchTerm}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.maindet").each(function () { - const novelName = loadedCheerio(this).find("h2").text(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novelUrl = loadedCheerio(this).find("h2 a").attr("href"); - - if (!novelUrl) return; - - novels.push({ - name: novelName, - url: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - label: "Sort By", - value: "popular", - options: [ - { label: "Default", value: "" }, - { label: "A-Z", value: "title" }, - { label: "Z-A", value: "titlereverse" }, - { label: "Latest Update", value: "update" }, - { label: "Latest Added", value: "latest" }, - { label: "Popular", value: "popular" }, - ], - type: FilterTypes.Picker, - }, - status: { - label: "Status", - value: "", - options: [ - { label: "All", value: "" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Hiatus", value: "hiatus" }, - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: [], - label: "Type", - options: [ - { label: "Light Novel (KR)", value: "light-novel-kr" }, - { label: "Web Novel", value: "web-novel" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genres: { - label: "Genres", - value: [], - options: [ - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Comedy", value: "comedy" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Harem", value: "harem" }, - { label: "Josei", value: "josei" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Romance", value: "romance" }, - { label: "School Life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Seinen", value: "seinen" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Smut", value: "smut" }, - { label: "Sports", value: "sports" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Tragedy", value: "tragedy" }, - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; -} - -export default new PandaMTL(); diff --git a/plugins/english/readlitenovel.ts b/plugins/english/readlitenovel.ts new file mode 100644 index 0000000..197b611 --- /dev/null +++ b/plugins/english/readlitenovel.ts @@ -0,0 +1,189 @@ +import { load as parseHTML } from "cheerio"; +import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { Plugin } from "@typings/plugin"; +import { Filters, FilterTypes } from "@libs/filterInputs"; +import dayjs from "dayjs"; + +class ReadLiteNovel implements Plugin.PluginBase { + id = "rln.app"; + name = "ReadLiteNovel"; + version = "1.0.0"; + icon = "src/en/readlitenovel/icon.png"; + site = "https://rln.app"; + + parseAgoDate(date: string | undefined){ //parseMadaraDate + if (date?.includes("ago")) { + const dayJSDate = dayjs(new Date()); // today + const timeAgo = date.match(/\d+/)?.[0] || ""; + const timeAgoInt = parseInt(timeAgo, 10); + + if (!timeAgo) return null; // there is no number! + + if (date.includes("hours ago") || date.includes("hour ago")) { + dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours + } + + if (date.includes("days ago") || date.includes("day ago")) { + dayJSDate.subtract(timeAgoInt, "days"); // go back N days + } + + if (date.includes("months ago") || date.includes("month ago")) { + dayJSDate.subtract(timeAgoInt, "months"); // go back N months + } + + if (date.includes("years ago") || date.includes("year ago")) { + dayJSDate.subtract(timeAgoInt, "years"); // go back N years + } + + return dayJSDate.toISOString(); + } + return null; // there is no "ago" so give up + }; + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions + ): Promise { + const link = `${this.site}/ranking/${filters.order.value}/${page}`; + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(".category-items li").each((i,el) => { + const novelUrl = loadedCheerio(el) + .find(".category-name a") + .attr("href"); + + if (!novelUrl) return; + const novelName = loadedCheerio(el) + .find(".category-name a") + .text() + .trim(); + let novelCover = loadedCheerio(el) + .find(".category-img img") + .attr("src"); + + if (novelCover && !isUrlAbsolute(novelCover)) { + novelCover = this.site + novelCover; + } + + const novel = { + path: novelUrl?.replace(this.site, ''), + name: novelName, + cover: novelCover, + }; + novels.push(novel); + }) + return novels; + }; + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio(".novel-title").text() || "Untitled", + cover: loadedCheerio(".novels-detail img").attr("src"), + summary: loadedCheerio('.empty-box').text().trim(), + chapters: [], + }; + + loadedCheerio('.novels-detail-right li').each((i,el) => { + const detailName = loadedCheerio(el).find('div:first').text(); + const detail = loadedCheerio(el).find('div:last'); + + switch(detailName){ + case "Status:": + novel.status = detail.text(); + break; + case "Genres:": + novel.genres = detail.find('a') + .map((i,el) => loadedCheerio(el).text()) + .toArray() + .join(', ') || detail.text(); + break; + case "Author(s):": + novel.author = detail.find('a') + .map((i,el) => loadedCheerio(el).text()) + .toArray() + .join(', ') || detail.text(); + break; + case "Translator:": + novel.artist = detail.text(); + break; + } + }) + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio(".cm-tabs-content li").each((i,el) => { + const chapterUrl = loadedCheerio(el).find("a").attr("href"); + if (!chapterUrl) return; + const chapterName = loadedCheerio(el).find("a").text().trim(); + const releaseDate = this.parseAgoDate( + loadedCheerio(el).find('svg').attr('data-bs-original-title')! + ); + + chapter.push({ + name: chapterName, + path: chapterUrl?.replace(this.site, ''), + releaseTime: releaseDate, + })}) + + novel.chapters = chapter; + return novel; + }; + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio("#chapterText").html() || ""; + + return chapterText; + }; + + async searchNovels(searchTerm: string, pageNo: number): Promise { + const url = this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`; + const result = await fetchApi(url); + const body = await result.json(); + const novels: Plugin.NovelItem[] = []; + + body.results.forEach((item: {link: string; original_title:string; image:string;}) => + novels.push({ + path: item.link, + name: item.original_title, + cover: item.image, + }) + ); + + return novels; + }; + + async fetchImage(url: string): Promise { + return fetchFile(url); + } + + filters = { + order: { + value: "top-rated", + label: "Order by", + options: [ + { label: "MOST VIEWED", value: "most-viewed" }, + { label: "TOP RATED", value: "top-rated" }, + { label: "BOOKMARKS", value: "subscribers" }, + { label: "NEW", value: "new" }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new ReadLiteNovel(); diff --git a/plugins/english/scribblehub.ts b/plugins/english/scribblehub.ts index 1b1f5b3..997318c 100644 --- a/plugins/english/scribblehub.ts +++ b/plugins/english/scribblehub.ts @@ -13,17 +13,17 @@ class ScribbleHubPlugin implements Plugin.PluginBase { parseNovels(loadedCheerio: CheerioAPI){ const novels: Plugin.NovelItem[] = []; - loadedCheerio('.search_main_box').each(function () { - const novelName = loadedCheerio(this).find('.search_title > a').text(); - const novelCover = loadedCheerio(this).find('.search_img > img').attr('src'); - const novelUrl = loadedCheerio(this).find('.search_title > a').attr('href'); + loadedCheerio('.search_main_box').each((i,el) => { + const novelName = loadedCheerio(el).find('.search_title > a').text(); + const novelCover = loadedCheerio(el).find('.search_img > img').attr('src'); + const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href'); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ''), } novels.push(novel); }); @@ -74,23 +74,20 @@ class ScribbleHubPlugin implements Plugin.PluginBase { return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetchApi(url); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url, + path: novelPath, + name: loadedCheerio('.fic_title').text() || 'Untitled', + cover: loadedCheerio('.fic_image > img').attr('src'), + summary: loadedCheerio('.wi_fic_desc').text(), + author: loadedCheerio('.auth_name_fic').text(), chapters: [], }; - - novel.name = loadedCheerio('.fic_title').text(); - - novel.cover = loadedCheerio('.fic_image > img').attr('src'); - - novel.summary = loadedCheerio('.wi_fic_desc').text(); novel.genres = loadedCheerio('.fic_genre') .map((i, el) => loadedCheerio(el).text()) @@ -103,13 +100,11 @@ class ScribbleHubPlugin implements Plugin.PluginBase { .includes('Ongoing') ? 'Ongoing' : 'Completed'; - - novel.author = loadedCheerio('.auth_name_fic').text(); - let formData = new FormData(); + const formData = new FormData(); formData.append('action', 'wi_getreleases_pagination'); formData.append('pagenum', '-1'); - formData.append('mypostid', novelUrl.split('/')[4]); + formData.append('mypostid', novelPath.split('/')[1]); const data = await fetchApi( `${this.site}wp-admin/admin-ajax.php`, @@ -130,7 +125,7 @@ class ScribbleHubPlugin implements Plugin.PluginBase { const timeAgo = date.match(/\d+/)?.[0] || ""; const timeAgoInt = parseInt(timeAgo, 10); - if (!timeAgo) return date; // there is no number! + if (!timeAgo) return null; // there is no number! if (date.includes("hours ago") || date.includes("hour ago")) { dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours @@ -149,16 +144,16 @@ class ScribbleHubPlugin implements Plugin.PluginBase { return null; }; - loadedCheerio('.toc_w').each(function () { - const chapterName = loadedCheerio(this).find('.toc_a').text(); - const releaseDate = loadedCheerio(this).find('.fic_date_pub').text(); - const chapterUrl = loadedCheerio(this).find('a').attr('href'); + loadedCheerio('.toc_w').each((i,el) => { + const chapterName = loadedCheerio(el).find('.toc_a').text(); + const releaseDate = loadedCheerio(el).find('.fic_date_pub').text(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); if (!chapterUrl) return; chapter.push({ name: chapterName, releaseTime: parseISODate(releaseDate), - url: chapterUrl, + path: chapterUrl.replace(this.site, ''), }); }); @@ -167,13 +162,13 @@ class ScribbleHubPlugin implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); - let chapterText = loadedCheerio('div.chp_raw').html() || ''; + const chapterText = loadedCheerio('div.chp_raw').html() || ''; return chapterText; } diff --git a/plugins/english/wtrlab.ts b/plugins/english/wtrlab.ts new file mode 100644 index 0000000..4fe9bb3 --- /dev/null +++ b/plugins/english/wtrlab.ts @@ -0,0 +1,286 @@ +import { Plugin } from "@typings/plugin"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { FilterTypes, Filters } from "@libs/filterInputs"; +import { load as parseHTML } from "cheerio"; + +class WTRLAB implements Plugin.PluginBase { + id = "WTRLAB"; + name = "WTR-LAB"; + site = "https://wtr-lab.com/"; + version = "1.0.0"; + icon = "src/en/wtrlab/icon.png"; + sourceLang = "en/" + + async popularNovels( + page: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + this.sourceLang + "novel-list?"; + link += `orderBy=${filters.order.value}`; + link += `&order=${filters.sort.value}`; + link += `&filter=${filters.storyStatus.value}`; + link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy. + + if(showLatestNovels){ + const response = await fetch('https://wtr-lab.com/api/home/recent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ page: page }) + }); + + const recentNovel: JsonNovel = await response.json(); + + // Parse novels from JSON + const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({ + name: datum.serie.data.title || "", + cover: datum.serie.data.image, + path: this.sourceLang + "serie-" + datum.serie.raw_id + "/" + datum.serie.slug || "", + })); + + return novels; + } + + else { + const body = await fetchApi(link).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + //console.log(link); + const novels: Plugin.NovelItem[] = loadedCheerio(".serie-item") + .map((index, element) => ({ + name: loadedCheerio(element).find(".title-wrap > a").text().replace(loadedCheerio(element).find(".rawtitle").text(), "") || "", + cover: loadedCheerio(element).find("img").attr("src"), + path: loadedCheerio(element).find("a").attr("href") || "", + })) + .get() + .filter((novel) => novel.name && novel.path); + return novels; + } + + } + + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + //console.log(this.site + novelPath); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio("h1.text-uppercase").text(), + cover: loadedCheerio(".img-wrap > img").attr("src"), + summary: loadedCheerio(".lead").text().trim(), + }; + + novel.genres = loadedCheerio('td:contains("Genre")') + .next() + .find('a') + .map((i,el) => loadedCheerio(el).text()) + .toArray() + .join(","); + + novel.author = loadedCheerio('td:contains("Author")') + .next() + .text() + .replace(/[\t\n]/g, ""); + + novel.status = loadedCheerio('td:contains("Status")') + .next() + .text() + .replace(/[\t\n]/g, ""); + + + const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ""; + const jsonData: NovelJson = JSON.parse(chapterJson); + + const chapters: Plugin.ChapterItem[] = jsonData.props.pageProps.serie.chapters.map((jsonChapter, chapterIndex) => ({ + name: "Chapter " + jsonChapter.slug + " " + jsonChapter.title, + path: this.sourceLang + "serie-" +jsonData.props.pageProps.serie.serie_data.raw_id + "/" + jsonData.props.pageProps.serie.serie_data.slug + "/chapter-" + jsonChapter.slug, // Assuming 'slug' is the intended path + releaseTime: jsonChapter.created_at.substring(0, 10), + chapterNumber: chapterIndex + 1 + })); + + novel.chapters = chapters; + + return novel; + } + + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ""; + const jsonData: NovelJson = JSON.parse(chapterJson); + + //console.log(body); + + const chapterContent = JSON.stringify(jsonData.props.pageProps.serie.chapter_data.data.body); + //console.log(chapterContent); + const parsedArray = JSON.parse(chapterContent); + let htmlString = ''; + + for (const text of parsedArray) { + htmlString += `

${text}

`; + } + + + + + return htmlString; +} + + async searchNovels(searchTerm: string): Promise { + const response = await fetch(this.site + "api/search", { + headers: { + "Content-Type": "application/json", + Referer: this.site + this.sourceLang, + Origin: this.site, + }, + method: "POST", + body: JSON.stringify({ text: searchTerm}), + }); + + const recentNovel: JsonNovel = await response.json(); + + + // Parse novels from JSON + const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({ + name: datum.data.title || "", + cover: datum.data.image, + path: this.sourceLang + "serie-" + datum.raw_id + "/" + datum.slug || "", + })); + + return novels; + } + + + fetchImage = fetchFile; + filters = { + order: { + value: "chapter", + label: "Order by", + options: [ + { label: "View", value: "view" }, + { label: "Name", value: "name" }, + { label: "Addition Date", value: "date" }, + { label: "Reader", value: "reader" }, + { label: "Chapter", value: "chapter" }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: "desc", + label: "Sort by", + options: [ + { label: "Descending", value: "desc" }, + { label: "Ascending", value: "asc" }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + value: "all", + label: "Status", + options: [ + { label: "All", value: "all" }, + { label: "Ongoing", value: "ongoing" }, + { label: "Completed", value: "completed" }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + + + +interface NovelJson { + props: Props; + page: string; +} + +interface Props { + pageProps: PageProps; + __N_SSP: boolean; +} + +interface PageProps { + serie: Serie; + server_time: Date; +} + +interface Serie { + serie_data: SerieData; + chapters: Chapter[]; + recommendation: SerieData[]; + chapter_data: ChapterData; + id: number; + raw_id: number; + slug: string; + data: Data; + is_default: boolean; + raw_type: string; +} + +interface Chapter { + serie_id: number; + id: number; + slug: string; + title: string; + name: string; + created_at: string; +} +interface ChapterData { + data: ChapterContent; +} +interface ChapterContent { + title: string; + body: string; +} + +interface SerieData { + serie_id?: number; + recommendation_id?: number; + score?: string; + id: number; + slug: string; + search_text: string; + status: number; + data: Data; + created_at: string; + updated_at: string; + view: number; + in_library: number; + rating: number | null; + chapter_count: number; + power: number; + total_rate: number; + user_status: number; + verified: boolean; + from: null; + raw_id: number; + genres?: number[]; +} + +interface Data { + title: string; + author: string; + description: string; + image: string; +} + +interface JsonNovel { + success: boolean; + data: Datum[]; +} +interface Datum { + serie: Serie; + chapters: Chapter[]; + updated_at: Date; + raw_id: number; + slug: string; + data: Data; + +} + + + + +export default new WTRLAB(); diff --git a/plugins/english/wuxiaworld.ts b/plugins/english/wuxiaworld.broken.ts similarity index 100% rename from plugins/english/wuxiaworld.ts rename to plugins/english/wuxiaworld.broken.ts diff --git a/plugins/french/chireads.ts b/plugins/french/chireads.ts index 7ba9aef..7d4b6ed 100644 --- a/plugins/french/chireads.ts +++ b/plugins/french/chireads.ts @@ -1,77 +1,155 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; +import { CheerioAPI, load } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; +import { Filters, FilterTypes } from "@libs/filterInputs"; +import dayjs from "dayjs"; +import { defaultCover } from "@libs/defaultCover"; +import { NovelStatus } from "@libs/novelStatus"; class ChireadsPlugin implements Plugin.PluginBase { - id: string; - name: string; - icon: string; - site: string; - version: string; + id = "chireads"; + name = "Chireads"; + icon = "src/fr/chireads/icon.png"; + site = "https://chireads.com"; + version = "1.0.2"; + async getCheerio(url: string): Promise { + const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); + const body = await r.text(); + const $ = load(body); + return ($); + } - constructor() { - this.id = "chireads.com"; - this.name = "Chireads"; - this.icon = "src/fr/chireads/icon.png"; - this.site = "https://chireads.com"; - this.version = "1.0.0"; - - } - parseNovels(loadedCheerio: CheerioAPI) { - return []; - } - async popularNovels(page: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}/category/translatedtales/page/${page}/`; - const result = await fetchApi(url); - const body = await result.text(); - const loadedCheerio = parseHTML(body); + async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise { + let url = this.site; + let tag: string = "all"; + if (showLatestNovels) + url += "/category/translatedtales/page/" + pageNo; + else { + if (filters && typeof filters.tag.value === "string" && filters.tag.value !== "all") + tag = filters.tag.value; + if (tag !== "all") + url += "/tag/" + tag + "/page/" + pageNo; + else if (pageNo > 1) + return []; + } + let $ = await this.getCheerio(url); let novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; - loadedCheerio("#content li").each(function () { - const novelName = loadedCheerio(this).find(".news-list-tit h5").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this) - .find(".news-list-tit h5 a") - .attr("href"); - - if (!novelUrl) return; - - const novel = { name: novelName, cover: novelCover, url: novelUrl }; - - novels.push(novel); - }); + if (showLatestNovels || tag !== "all") { + let loop = 1; + if (showLatestNovels) + loop = 2; + for (let i = 0; i < loop; i++) { + if (i === 1) + $ = await this.getCheerio(this.site + "/category/original/page/" + pageNo); + let romans = $(".romans-content li"); + if (!romans.length) + romans = $("#content li"); + romans.each((i, elem) => { + const novelName = $(elem).contents().find("div").first().text().trim(); + const novelCover = $(elem).find("div").first().find("img").attr("src"); + const novelUrl = $(elem).find("div").first().find("a").attr("href") + + if (novelUrl){ + novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") }; + novels.push(novel); + } + }); + } + } + else { + const populaire = $(":contains(\"Populaire\")").last().parent().next().find("li > div") + if (populaire.length === 12) // pc + { + let novelCover : string | undefined; + let novelName : string | undefined; + let novelUrl : string | undefined; + populaire.each((i, elem) => { + if (i % 2 === 0) + novelCover = $(elem).find("img").attr("src"); + else { + novelName = $(elem).text().trim(); + novelUrl = $(elem).find("a").attr("href"); + + if (!novelUrl) return; + + novel = { name: novelName, cover: novelCover || defaultCover, path: novelUrl.replace(this.site, "") }; + + novels.push(novel); + } + }); + } + else // mobile + { + const imgs = populaire.find("div.popular-list-img img"); + const txts = populaire.find("div.popular-list-name"); + txts.each((i, elem) => { + const novelName = $(elem).text().trim(); + const novelCover = $(imgs[i]).attr("src"); + const novelUrl = $(elem).find("a").attr("href"); + + if (novelUrl){ + novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") }; + novels.push(novel); + } + }); + } + } return novels; }; - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const novel: Plugin.SourceNovel = { url }; - const result = await fetchApi(url); - const body = await result.text(); + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { path: novelPath, name: "Sans titre" }; - let loadedCheerio = parseHTML(body); + let $ = await this.getCheerio(this.site + novelPath); - novel.name = loadedCheerio(".inform-title").text().trim(); - novel.cover = loadedCheerio(".inform-product img").attr("src"); - novel.summary = loadedCheerio(".inform-inform-txt").text().trim(); + novel.name = $(".inform-product-txt").first().text().trim() || $(".inform-title").text().trim(); + novel.cover = $(".inform-product img").attr("src") || $(".inform-product-img img").attr("src") || defaultCover; + novel.summary = $(".inform-inform-txt").text().trim() || $(".inform-intr-txt").text().trim(); + + let infos = $("div.inform-product-txt > div.inform-intr-col").text().trim() || $("div.inform-inform-data > h6").text().trim(); + if (infos.includes("Auteur : ")) + novel.author = infos.substring(infos.indexOf("Auteur : ") + 9, infos.indexOf("Statut de Parution : ")).trim(); + else if (infos.includes("Fantrad : ")) + novel.author = infos.substring(infos.indexOf("Fantrad : ") + 10, infos.indexOf("Statut de Parution : ")).trim(); + else + novel.author = "Inconnu"; + switch (infos.substring(infos.indexOf("Statut de Parution : ") + 21).toLowerCase()) { + case "en pause": + novel.status = NovelStatus.OnHiatus; + break; + case "complet": + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Ongoing; + break; + } let chapters: Plugin.ChapterItem[] = []; - loadedCheerio(".chapitre-table a").each(function () { - const chapterName = loadedCheerio(this).text().trim(); - const releaseDate = null; - const chapterUrl = loadedCheerio(this).attr("href"); + let chapterList = $(".chapitre-table a"); + if (!chapterList.length) + { + $("div.inform-annexe-list").first().remove(); + chapterList = $(".inform-annexe-list").find("a"); + } + chapterList.each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr("href"); + const releaseDate = dayjs(chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1)).format("DD MMMM YYYY"); - if (!chapterUrl) return; - - chapters.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); + if (chapterUrl){ + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ""), + }); + } }); novel.chapters = chapters; @@ -80,42 +158,108 @@ class ChireadsPlugin implements Plugin.PluginBase { }; async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); - const body = await result.text(); + const $ = await this.getCheerio(this.site + chapterUrl); - let loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#content").html() || ""; + const chapterText = $("#content").html() || ""; return chapterText; }; - async searchNovels(searchTerm: string): Promise { - const url = `${this.site}/search?x=0&y=0&name=${searchTerm}`; - const result = await fetchApi(url); - const body = await result.text(); - let loadedCheerio = parseHTML(body); - + async searchNovels(searchTerm: string, pageNo: number): Promise { + if (pageNo !== 1) + return []; let novels: Plugin.NovelItem[] = []; - loadedCheerio("#content li").each(function () { - const novelName = loadedCheerio(this).find(".news-list-tit h5").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this) - .find(".news-list-tit h5 a") - .attr("href"); + let i = 1; + let finised = false; + while ( !finised ) + { + await this.popularNovels(i, { showLatestNovels: true, filters: undefined } ).then((res) => { + if (res.length === 0) + finised = true; + novels.push(...res); + }); + i++; + } - if (!novelUrl) return; + novels = novels.filter((novel) => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + ) + ); - const novel = { name: novelName, cover: novelCover, url: novelUrl }; - - novels.push(novel); - }); return novels; }; - async fetchImage(url: string): Promise{ - return await fetchFile(url); - } + + fetchImage = fetchFile; + + filters = { + tag: { + type: FilterTypes.Picker, + label: "Tag", + value: "all", + options: [ + { label: "Tous", value: "all" }, + { label: "Arts martiaux", value: "arts-martiaux" }, + { label: "De faible à fort", value: "de-faible-a-fort" }, + { label: "Adapté en manhua", value: "adapte-en-manhua" }, + { label: "Cultivation", value: "cultivation" }, + { label: "Action", value: "action" }, + { label: "Aventure", value: "aventure" }, + { label: "Monstres", value: "monstres" }, + { label: "Xuanhuan", value: "xuanhuan" }, + { label: "Fantastique", value: "fantastique" }, + { label: "Adapté en Animé", value: "adapte-en-anime" }, + { label: "Alchimie", value: "alchimie" }, + { label: "Éléments de jeux", value: "elements-de-jeux" }, + { label: "Calme Protagoniste", value: "calme-protagoniste" }, + { label: "Protagoniste intelligent", value: "protagoniste-intelligent" }, + { label: "Polygamie", value: "polygamie" }, + { label: "Belle femelle Lea", value: "belle-femelle-lea" }, + { label: "Personnages arrogants", value: "personnages-arrogants" }, + { label: "Système de niveau", value: "systeme-de-niveau" }, + { label: "Cheat", value: "cheat" }, + { label: "Protagoniste génie", value: "protagoniste-genie" }, + { label: "Comédie", value: "comedie" }, + { label: "Gamer", value: "gamer" }, + { label: "Mariage", value: "mariage" }, + { label: "seeking Protag", value: "seeking-protag" }, + { label: "Romance précoce", value: "romance-precoce" }, + { label: "Croissance accélérée", value: "croissance-acceleree" }, + { label: "Artefacts", value: "artefacts" }, + { label: "Intelligence artificielle", value: "intelligence-artificielle" }, + { label: "Mariage arrangé", value: "mariage-arrange" }, + { label: "Mature", value: "mature" }, + { label: "Adulte", value: "adulte" }, + { label: "Administrateur de système", value: "administrateur-de-systeme" }, + { label: "Beau protagoniste", value: "beau-protagoniste" }, + { label: "Protagoniste charismatique", value: "protagoniste-charismatique" }, + { label: "Protagoniste masculin", value: "protagoniste-masculin" }, + { label: "Démons", value: "demons" }, + { label: "Reincarnation", value: "reincarnation" }, + { label: "Académie", value: "academie" }, + { label: "Cacher les vraies capacités", value: "cacher-les-vraies-capacites" }, + { label: "Protagoniste surpuissant", value: "protagoniste-surpuissant" }, + { label: "Joueur", value: "joueur" }, + { label: "Protagoniste fort dès le départ", value: "protagoniste-fort-des-le-depart" }, + { label: "Immortels", value: "immortels" }, + { label: "Cultivation rapide", value: "cultivation-rapide" }, + { label: "Harem", value: "harem" }, + { label: "Assasins", value: "assasins" }, + { label: "De pauvre à riche", value: "de-pauvre-a-riche" }, + { label: "Système de classement de jeux", value: "systeme-de-classement-de-jeux" }, + { label: "Capacités spéciales", value: "capacites-speciales" }, + { label: "Vengeance", value: "vengeance" }, + ], + } + } satisfies Filters; }; export default new ChireadsPlugin(); \ No newline at end of file diff --git a/plugins/french/noveldeglace.ts b/plugins/french/noveldeglace.ts index 262488a..37d6911 100644 --- a/plugins/french/noveldeglace.ts +++ b/plugins/french/noveldeglace.ts @@ -1,180 +1,334 @@ -import { load } from "cheerio"; +import { load, CheerioAPI } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; import { NovelStatus } from "@libs/novelStatus"; +import { Filters, FilterTypes } from "@libs/filterInputs"; +import { defaultCover } from "@libs/defaultCover"; +import dayjs from "dayjs"; class NovelDeGlacePlugin implements Plugin.PluginBase{ - id: string; - name: string; - icon: string; - site: string; - version: string; + id = "noveldeglace"; + name = "NovelDeGlace"; + icon = "src/fr/noveldeglace/icon.png"; + site = "https://noveldeglace.com/"; + version = "1.0.2"; + async getCheerio(url: string): Promise { + const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); + if (!r.ok) return (undefined); + const body = await r.text(); + const loadedCheerio = load(body); + return (loadedCheerio); + } - constructor() { - this.id = "NDG.com"; - this.name = "NovelDeGlace"; - this.icon = "src/fr/noveldeglace/icon.png"; - this.site = "https://noveldeglace.com/"; - this.version = "1.0.0"; - - } - - async popularNovels(page: number): Promise { - let url = this.site + "roman"; - const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' },}); - const body = await result.text(); - let loadedCheerio = load(body); - + parseNovels($: CheerioAPI, showLatestNovels: boolean | undefined): Plugin.NovelItem[] { let novels: Plugin.NovelItem[] = []; - loadedCheerio("article").each(function () { - const novelName = loadedCheerio(this).find("h2").text().trim(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h2 > a").attr("href"); + $("article").each((i, el) => { + const cheerio = $(el); + const novelName = cheerio.find("h2").text().trim(); + const novelCover = cheerio.find("img").attr("src"); + let novelUrl: string | undefined; + if (showLatestNovels) + novelUrl = cheerio.find("span.Roman > a").attr("href"); + else + novelUrl = cheerio.find("h2 > a").attr("href"); - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); + if (novelUrl){ + const novel:Plugin.NovelItem = { + name: novelName, + path: novelUrl.replace(this.site, ""), + cover: novelCover, + }; + novels.push(novel); + } }); return novels; + } + + async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise { + let url = this.site; + if (showLatestNovels) + url += "chapitre" + else + { + let cat_gen: string = "all"; + if (filters && typeof filters.categorie_genre.value == "string") + cat_gen = filters.categorie_genre.value; + if (cat_gen != "all" && cat_gen != "categorie_roman" && cat_gen != "genre") + { + if (cat_gen[0] == "c") + url += "categorie_roman/" + cat_gen.substring(2); + else if (cat_gen[0] == "g") + url += "genre/" + cat_gen.substring(2); + } + else if (pageNo > 1) + return ([]); // when asking for all novels, there is only 1 page + else + url += "roman"; + } + url += "/page/" + pageNo; + let $ = await this.getCheerio(url); + if (!$) + return ([]); + return this.parseNovels($, showLatestNovels); }; - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); - const body = await result.text(); + async parseNovel(novelPath: string): Promise { + const $ = await this.getCheerio(this.site + novelPath); + if (!$) + throw new Error("Failed to load page (open in web view)"); + + let novel: Plugin.SourceNovel = { path: novelPath, name: "Untitled" }; + + novel.name = ($("div.entry-content > div > strong")[0] + .nextSibling as Text | null) + ?.nodeValue + ?.trim() || "Untitled"; + + novel.cover = $(".su-row > div > div > img") + .attr("src") || defaultCover; + + let novelInfos = $("div[data-title=Tomes] >") + .toArray(); + novelInfos.pop(); + novelInfos.shift(); + novel.summary = $("div[data-title=Synopsis]") + .text().trim() + + "\n\n" + novelInfos.map((el) => $(el).text()).join("\n").trim(); + + novel.author = $("strong:contains('Auteur :')") + .parent() + .text() + .replace("Auteur : ", "") + .trim(); + + novel.artist = $("strong:contains('Illustrateur :')") + .parent() + .text() + .replace("Illustrateur :", "") + .trim(); + + const categorie = $(".categorie") + .text() + .replace("Catégorie :", "") + .trim(); + const genres = $(".genre") + .text() + .replace("Genre :", "") + .replace(/, /g, ",") + .trim(); + if (categorie && categorie != "Autre") + novel.genres = categorie; + if (genres) + novel.genres = novel.genres ? novel.genres + "," + genres : genres; + + let status = $("strong:contains('Statut :')") + .parent() + .attr("class"); + switch (status) { + case "type etat0": + case "type etat1": + novel.status = NovelStatus.Ongoing; + break; + case "type etat4": + novel.status = NovelStatus.OnHiatus; + break; + case "type etat5": + novel.status = NovelStatus.Completed; + break; + case "type etat6": + novel.status = NovelStatus.Cancelled; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + + let novelChapters: Plugin.ChapterItem[] = []; + + const volumes = $("div[data-title=Tomes] > div") + .last() + .contents(); + const hasMultipleVolumes = volumes.length > 1; - let loadedCheerio = load(body); + let chapterName = "" + let site = this.site; + const monthMapping: { [key: string]: string } = { janvier: "January", fevrier: "February", mars: "March", avril: "April", mai: "May", juin: "June", juillet: "July", aout: "August", septembre: "September", octobre: "October", novembre: "November", decembre: "December"}; + volumes.each((volumeIndex: number, el) => { + if (hasMultipleVolumes) + chapterName = "T." + (volumeIndex + 1) + " "; + $(el) + .find(".chpt") + .each((chapterIndex: number, el) => { + const cheerio = $(el); + const newChapterName = chapterName + cheerio + .find("a") + .first() + .text().trim() || ""; + if (!cheerio.find("i.fa").length) // no parts + { + const dateHtml = cheerio.html()?.substring(cheerio.html()?.indexOf("") || 0) || ""; + let releaseDate = dateHtml?.substring(dateHtml.indexOf("(") + 1, dateHtml.indexOf(")")) || undefined; + if (releaseDate) + { + try { + const [day, month, year] = releaseDate.split(" "); + releaseDate = dayjs(`${day} ${monthMapping[month.normalize("NFD").replace(/[\u0300-\u036f]/g, "")]} ${year}`, "DD MMMM YYYY").format("YYYY-MM-DD"); + } catch (e) {} + } + const chapterUrl = cheerio.find("a").attr("href"); + if (chapterUrl) + { + const chapter: Plugin.ChapterItem = { + name: newChapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(site, ""), + chapterNumber: chapterIndex, + }; + + novelChapters.push(chapter); + } + } + else // has parts that needs to be added individually + { + const items = cheerio + .find("i") + .parent() + .next() + .html() + ?.split("") || []; + items?.shift(); + const dates: string[] = []; + items?.forEach((item) => { // there is a date on every publish parts + dates.push(item.substring(item.indexOf("(") + 1, item.indexOf(")"))); + }); + const hrefs: string[] = []; + cheerio + .find("i") + .parent() + .next() + .find("a") + .each(function () { + hrefs.push(this["attribs"]["href"]); + }); + if (dates.length == hrefs.length) + dates.forEach((date, index) => { + let releaseDate = date; + try { + const [day, month, year] = date.split(" "); + releaseDate = dayjs(`${day} ${monthMapping[month.normalize("NFD").replace(/[\u0300-\u036f]/g, "")]} ${year}`, "DD MMMM YYYY").format("YYYY-MM-DD"); + } catch (e) {} + const chapter: Plugin.ChapterItem = { + name: newChapterName + " (" + (index + 1) + ")", + releaseTime: releaseDate, + path: hrefs[index].replace(site, ""), + chapterNumber: chapterIndex + (index + 1) / 1000, + }; + novelChapters.push(chapter); + } + ); + } + }); + }); + + novel.chapters = novelChapters; + + return novel; + }; - let novel: Plugin.SourceNovel = { url }; + async parseChapter(chapterPath: string): Promise { + const $ = await this.getCheerio(this.site + chapterPath); + if (!$) + throw new Error("Failed to load page (open in web view)"); - novel.name = (loadedCheerio("div.entry-content > div > strong" - )[0].nextSibling as Text | null)?.nodeValue?.trim() || ''; - - novel.cover = loadedCheerio(".su-row > div > div > img").attr("src"); - - novel.summary = loadedCheerio("div[data-title=Synopsis]").text(); - - novel.author = loadedCheerio("strong:contains('Auteur :')" - ).parent().text().replace("Auteur : ", "").trim(); - - // novel.artist = loadedCheerio("strong:contains('Illustrateur :')" - // ).parent().text().replace("Illustrateur : ", "").trim(); - - const categorie = loadedCheerio(".categorie") - .text() - .replace("Catégorie :", "") - .trim(); - const genres = loadedCheerio(".genre") - .text() - .replace("Genre :", "") - .replace(/, /g, ",") - .trim(); - if (categorie && categorie != "Autre") - novel.genres = categorie; - if (genres) - novel.genres = novel.genres ? novel.genres + "," + genres : genres; - - let status = loadedCheerio("strong:contains('Statut :')" - ).parent().attr("class"); - switch (status) { - case "type etat0": - novel.status = NovelStatus.Ongoing; - break; - case "type etat1": - novel.status = NovelStatus.Ongoing; - break; - case "type etat4": - novel.status = NovelStatus.OnHiatus; - break; - case "type etat5": - novel.status = NovelStatus.Completed; - break; - case "type etat6": - novel.status = NovelStatus.Cancelled; - break; - default: - novel.status = NovelStatus.Unknown; - break; - } - - let novelChapters: Plugin.ChapterItem[] = []; - - loadedCheerio(".chpt").each(function () { - const chapterName = loadedCheerio(this).find("a").text().trim(); - const releaseDate = null; - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; - - novelChapters.push(chapter); - }); - - novel.chapters = novelChapters; - - return novel; - }; - - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; - const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); - const body = await result.text(); - - let loadedCheerio = load(body); - - loadedCheerio(".mistape_caption").remove(); - let chapterText = loadedCheerio(".chapter-content").html() || "Chapitre introuvable"; + $(".mistape_caption").remove(); + let chapterText = $(".chapter-content").html() + || $(".entry-content").html() + || ""; return chapterText; }; - async searchNovels(searchTerm: string): Promise { + async searchNovels(searchTerm: string, num: number): Promise { + if (num !== 1) return ([]); // only 1 page of results let url = this.site + "roman"; - const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); - const body = await result.text(); - - let loadedCheerio = load(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio("article").each(function () { - const novelName = loadedCheerio(this).find("h2").text().trim(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("h2 > a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); + const $ = await this.getCheerio(url); + if (!$) + throw new Error("Failed to load page (open in web view)"); + + let novels = this.parseNovels($, false); novels = novels.filter((novel) => - novel.name.toLowerCase().includes(searchTerm.toLowerCase()) + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + ) ); return novels; }; - async fetchImage(url: string): Promise{ - return await fetchFile(url); - } + + fetchImage = fetchFile; + + filters = { + categorie_genre: { + type: FilterTypes.Picker, + label: "Catégorie/Genre", + value: "all", + options: [ + { label: "Tous", value: "all" }, + { label: "═══CATÉGORIES═══", value: "categorie_roman" }, + { label: "Seinen", value: "c_seinen" }, + { label: "Shonen", value: "c_shonen" }, + { label: "Original", value: "c_original" }, + { label: "Yuri", value: "c_yuri" }, + { label: "Autre", value: "c_autre" }, + { label: "Fille", value: "c_fille" }, + { label: "Roman pour Adulte", value: "c_roman-pour-adulte" }, + { label: "Xuanhuan", value: "c_xuanhuan" }, + { label: "Yaoi", value: "c_yaoi" }, + { label: "═══GENRES═══", value: "genre" }, + { label: "Action", value: "g_action" }, + { label: "Aventure", value: "g_aventure" }, + { label: "Comédie", value: "g_comedie" }, + { label: "Drame", value: "g_drame" }, + { label: "Fantastique", value: "g_fantastique" }, + { label: "Harem", value: "g_harem" }, + { label: "Psychologique", value: "g_psychologique" }, + { label: "Romance", value: "g_romance" }, + { label: "Ecchi", value: "g_ecchi" }, + { label: "Mature", value: "g_mature" }, + { label: "Surnaturel", value: "g_surnaturel" }, + { label: "Vie scolaire", value: "g_vie-scolaire" }, + { label: "Adulte", value: "g_adulte" }, + { label: "Tragédie", value: "g_tragedie" }, + { label: "Arts Martiaux", value: "g_arts-martiaux" }, + { label: "Pas de harem", value: "g_pas-de-harem" }, + { label: "Tranche de vie", value: "g_tranche-de-vie" }, + { label: "Mecha", value: "g_mecha" }, + { label: "Sci-fi", value: "g_sci-fi" }, + { label: "Science-Fiction", value: "g_science-fiction" }, + { label: "Anti-Héros", value: "g_anti-heros" }, + { label: "Horreur", value: "g_horreur" }, + { label: "Insectes", value: "g_insectes" }, + { label: "Mystère", value: "g_mystere" }, + { label: "Lolicon", value: "g_lolicon" }, + { label: "Shoujo Ai", value: "g_shoujo-ai" }, + { label: "Smut", value: "g_smut" }, + { label: "Xuanhuan", value: "g_xuanhuan" }, + { label: "Shotacon", value: "g_shotacon" }, + ], + }, + } satisfies Filters; } export default new NovelDeGlacePlugin(); \ No newline at end of file diff --git a/plugins/indonesian/indowebnovel.ts b/plugins/indonesian/indowebnovel.broken.ts similarity index 100% rename from plugins/indonesian/indowebnovel.ts rename to plugins/indonesian/indowebnovel.broken.ts diff --git a/plugins/indonesian/novelringan.ts b/plugins/indonesian/novelringan.broken.ts similarity index 100% rename from plugins/indonesian/novelringan.ts rename to plugins/indonesian/novelringan.broken.ts diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.broken.ts similarity index 100% rename from plugins/indonesian/sakuranovel.ts rename to plugins/indonesian/sakuranovel.broken.ts diff --git a/plugins/japanese/Syosetu.ts b/plugins/japanese/Syosetu.broken.ts similarity index 100% rename from plugins/japanese/Syosetu.ts rename to plugins/japanese/Syosetu.broken.ts diff --git a/plugins/korean/Agitoon.ts b/plugins/korean/Agitoon.broken.ts similarity index 100% rename from plugins/korean/Agitoon.ts rename to plugins/korean/Agitoon.broken.ts diff --git a/plugins/portuguese/lightnovelbrasil.ts b/plugins/portuguese/lightnovelbrasil.broken.ts similarity index 100% rename from plugins/portuguese/lightnovelbrasil.ts rename to plugins/portuguese/lightnovelbrasil.broken.ts diff --git a/plugins/russian/LitSpace.ts b/plugins/russian/LitSpace.ts index eeb6a83..2449805 100644 --- a/plugins/russian/LitSpace.ts +++ b/plugins/russian/LitSpace.ts @@ -14,7 +14,10 @@ class freedlit implements Plugin.PluginBase { async popularNovels( page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + "/get-books/all/list/" + page + "?sort="; url += showLatestNovels ? "recent" : filters?.sort?.value || "popular"; @@ -38,27 +41,30 @@ class freedlit implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio("#bookListBlock > div > div").each(function () { - const name = loadedCheerio(this).find("div > h4 > a").text()?.trim(); - const cover = loadedCheerio(this) + loadedCheerio("#bookListBlock > div > div").each((index, element) => { + const name = loadedCheerio(element).find("div > h4 > a").text()?.trim(); + const cover = loadedCheerio(element) .find("div > a > img") .attr("src") ?.trim(); - const url = loadedCheerio(this).find("div > h4 > a").attr("href")?.trim(); + const url = loadedCheerio(element) + .find("div > h4 > a") + .attr("href") + ?.trim(); if (!name || !url) return; - novels.push({ name, cover, url }); + novels.push({ name, cover, path: url.replace(this.site, "") }); }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const body = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, name: loadedCheerio(".book-info h4").text(), cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(), summary: loadedCheerio("#nav-home").text()?.trim(), @@ -77,11 +83,11 @@ class freedlit implements Plugin.PluginBase { if (!name || !url) return; const releaseDate = loadedCheerio(element).find('span[class="date"]').text(); - chapters.push({ - name, - url, - releaseTime: this.parseDate(releaseDate), - chapterNumber: chapterIndex + 1 + chapters.push({ + name, + path: url.replace(this.site, ""), + releaseTime: this.parseDate(releaseDate), + chapterNumber: chapterIndex + 1, }); }); @@ -89,8 +95,8 @@ class freedlit implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); loadedCheerio("div.mobile-block").remove(); @@ -101,18 +107,18 @@ class freedlit implements Plugin.PluginBase { } async searchNovels(searchTerm: string): Promise { - const url = `${this.site}/search?query=${searchTerm}&type=all`; + const url = this.site + "/search?query=" + searchTerm + "&type=all"; const body = await fetchApi(url).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio("#bookListBlock > div").each(function () { - const name = loadedCheerio(this).find("h4 > a").text()?.trim(); - const cover = loadedCheerio(this).find("a > img").attr("src")?.trim(); - const url = loadedCheerio(this).find("h4 > a").attr("href")?.trim(); + loadedCheerio("#bookListBlock > div").each((index, element) => { + const name = loadedCheerio(element).find("h4 > a").text()?.trim(); + const cover = loadedCheerio(element).find("a > img").attr("src")?.trim(); + const url = loadedCheerio(element).find("h4 > a").attr("href")?.trim(); if (!name || !url) return; - novels.push({ name, cover, url }); + novels.push({ name, cover, path: url.replace(this.site, "") }); }); return novels; diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index b6889bf..294f37d 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -53,14 +53,15 @@ class AuthorToday implements Plugin.PluginBase { cover: novel.coverUrl ? "https://cm.author.today/content/" + novel.coverUrl : defaultCover, - url: this.site + "/work/" + novel.id, + path: "/work/" + novel.id, }), ); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const workID = novelUrl.split("/")[4]; + + async parseNovel(novelPath: string): Promise { + const workID = novelPath.split("/")[2]; const result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, { headers: { Authorization: token, @@ -69,7 +70,7 @@ class AuthorToday implements Plugin.PluginBase { const book = (await result.json()) as responseBook; const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, name: book.title, cover: book.coverUrl ? book.coverUrl.split("?")[0] : defaultCover, genres: book.tags?.join(", "), @@ -84,10 +85,13 @@ class AuthorToday implements Plugin.PluginBase { status: book.isFinished ? NovelStatus.Completed : NovelStatus.Ongoing, }; - if (book.annotation) novel.summary += book.annotation + "\n"; - if (book.authorNotes) novel.summary += "Примечания автора:\n" + book.authorNotes; + if (book.annotation) { + novel.summary += book.annotation + "\n"; + } + if (book.authorNotes) { + novel.summary += "Примечания автора:\n" + book.authorNotes; + } - // all chapters const chaptersRaw = await fetchApi(`${apiUrl}v1/work/${workID}/content`, { headers: { Authorization: token, @@ -96,12 +100,15 @@ class AuthorToday implements Plugin.PluginBase { const chaptersJSON = (await chaptersRaw.json()) as ChaptersEntity[]; const chapters: Plugin.ChapterItem[] = []; + chaptersJSON.forEach((chapter, chapterIndex) => { if (chapter.isAvailable && !chapter.isDraft) { chapters.push({ - name: chapter.title || 'Глава ' + (chapterIndex + 1), - url: `${apiUrl}v1/work/${workID}/chapter/${chapter.id}/text`, - releaseTime: dayjs(chapter.publishTime || chapter.lastModificationTime).format("LLL"), + name: chapter.title || "Глава " + (chapterIndex + 1), + path: `v1/work/${workID}/chapter/${chapter.id}/text`, + releaseTime: dayjs( + chapter.publishTime || chapter.lastModificationTime, + ).format("LLL"), chapterNumber: (chapter.sortOrder || chapterIndex) + 1, }); } @@ -111,8 +118,8 @@ class AuthorToday implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl, { + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(apiUrl + chapterPath, { headers: { Authorization: token, }, @@ -159,15 +166,14 @@ class AuthorToday implements Plugin.PluginBase { .text() .trim(); let cover = loadedCheerio(element).find("img").attr("src"); - const url = loadedCheerio(element) + const path = loadedCheerio(element) .find('div[class="book-title"] a') - .attr("href") - ?.split("/")?.[2]; + .attr("href"); cover = cover ? cover.split("?")[0] : defaultCover; - if (!url) return; - novels.push({ name, cover, url: this.site + "/work/" + url }); + if (!path) return; + novels.push({ name, cover, path }); }); return novels; @@ -193,7 +199,7 @@ class AuthorToday implements Plugin.PluginBase { label: "Жанры", value: "", options: [ - { label: "Все", value:"" }, + { label: "Все", value: "" }, { label: "Альтернативная история", value: "sf-history" }, { label: "Антиутопия", value: "dystopia" }, { label: "Бизнес-литература", value: "biznes-literatura" }, diff --git a/plugins/russian/bookriver.ts b/plugins/russian/bookriver.ts index 2170e77..dcc8823 100644 --- a/plugins/russian/bookriver.ts +++ b/plugins/russian/bookriver.ts @@ -14,10 +14,15 @@ class Bookriver implements Plugin.PluginBase { async popularNovels( pageNo: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + `/genre?page=${pageNo}&perPage=24&sortingType=`; - url += showLatestNovels ? "last-update" : filters?.sort?.value || "bestseller"; + let url = this.site + "/genre?page=" + pageNo + "&perPage=24&sortingType="; + url += showLatestNovels + ? "last-update" + : filters?.sort?.value || "bestseller"; if (filters?.genres?.value?.length) { url += "&g=" + filters.genres.value.join(","); @@ -34,15 +39,15 @@ class Bookriver implements Plugin.PluginBase { novels.push({ name: novel.name, cover: novel.coverImages[0].url, - url: this.site + "/book/" + novel.slug, + path: "/book/" + novel.slug, }), ); } return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const result = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(result); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); @@ -50,8 +55,8 @@ class Bookriver implements Plugin.PluginBase { const book = json.props.pageProps.state.book?.bookPage; const novel: Plugin.SourceNovel = { - url: novelUrl, - name: book?.name, + path: novelPath, + name: book?.name || "", cover: book?.coverImages[0].url, summary: book?.annotation, author: book?.author?.name, @@ -67,7 +72,7 @@ class Bookriver implements Plugin.PluginBase { if (chapter.available) { chapters.push({ name: chapter.name, - url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId, + path: "/reader/" + book?.slug + "/" + chapter.chapterId, releaseTime: dayjs( chapter.firstPublishedAt || chapter.createdAt || undefined, ).format("LLL"), @@ -79,9 +84,9 @@ class Bookriver implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { + async parseChapter(chapterPath: string): Promise { const url = "https://api.bookriver.ru/api/v1/books/chapter/text/"; - const result = await fetchApi(url + chapterUrl.split("/").pop()); + const result = await fetchApi(url + chapterPath.split("/").pop()); const json = (await result.json()) as responseChapter; let chapterText = json.data.content || "Конец произведения"; @@ -96,7 +101,9 @@ class Bookriver implements Plugin.PluginBase { searchTerm: string, pageNo: number | undefined = 1, ): Promise { - const url = `https://api.bookriver.ru/api/v1/search/autocomplete?keyword=${searchTerm}&page=${pageNo}&perPage=10`; + const url = + "https://api.bookriver.ru/api/v1/search/autocomplete?keyword=" + searchTerm + + "&page=" + pageNo + "&perPage=10"; const result = await fetchApi(url); const json = (await result.json()) as responseSearch; @@ -105,12 +112,13 @@ class Bookriver implements Plugin.PluginBase { novels.push({ name: novel.name, cover: novel.coverImages[0].url, - url: this.site + "/book/" + novel.slug, + path: "/book/" + novel.slug, }), ); return novels; } + fetchImage = fetchFile; filters = { diff --git a/plugins/russian/ficbook.ts b/plugins/russian/ficbook.ts index 881527c..f5d937d 100644 --- a/plugins/russian/ficbook.ts +++ b/plugins/russian/ficbook.ts @@ -41,25 +41,24 @@ class ficbook implements Plugin.PluginBase { : defaultCover; if (!name || !url) return; - novels.push({ name, cover, url: this.site + url.replace(/\?.*/g, "") }); + novels.push({ name, cover, path: url.replace(/\?.*/g, "") }); }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const result = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(result); const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, + name: ( + loadedCheerio('h1[itemprop="headline"]').text() || + loadedCheerio('h1[itemprop="name"]').text() || + "" + ).trim(), }; - - novel.name = ( - loadedCheerio('h1[itemprop="headline"]').text() || - loadedCheerio('h1[itemprop="name"]').text() - ).trim(); - novel.cover = loadedCheerio('meta[property="og:image"]').attr("content"); novel.summary = loadedCheerio('div[itemprop="description"]').text().trim(); novel.author = loadedCheerio('a[itemprop="author"]').text(); @@ -91,7 +90,7 @@ class ficbook implements Plugin.PluginBase { const name = loadedCheerio(".title-area > h2").text(); const releaseTime = loadedCheerio(".part-date > span").attr("title"); - if (name) chapters.push({ name, url: novelUrl, releaseTime }); + if (name) chapters.push({ name, path: novelPath, releaseTime }); } else { loadedCheerio("li.part").each((chapterIndex, element) => { const name = loadedCheerio(element).find("h3").text(); @@ -99,22 +98,21 @@ class ficbook implements Plugin.PluginBase { if (!name || !url) return; const releaseTime = loadedCheerio(element).find("div > span").attr("title"); - chapters.push({ - name, - url: this.site + url, - releaseTime, + chapters.push({ + name, + path: url.replace(this.site, ""), + releaseTime, chapterNumber: chapterIndex + 1, }); }); - } novel.chapters = chapters; return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(result); let chapterText = ""; @@ -154,12 +152,12 @@ class ficbook implements Plugin.PluginBase { json.data?.data?.forEach((novel) => { const name = novel.title.trim(); - const url = this.site + "/readfic/" + novel.slug; + const path = "/readfic/" + novel.slug; const cover = novel.cover ? "https://images.ficbook.net/fanfic-covers/" + novel.cover : defaultCover; - novels.push({ name, cover, url }); + novels.push({ name, cover, path }); }); return novels; diff --git a/plugins/russian/jaomix.ts b/plugins/russian/jaomix.ts index a13a5fd..bef5d04 100644 --- a/plugins/russian/jaomix.ts +++ b/plugins/russian/jaomix.ts @@ -14,7 +14,10 @@ class Jaomix implements Plugin.PluginBase { async popularNovels( pageNo: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + "/?searchrn"; @@ -45,34 +48,33 @@ class Jaomix implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio('div[class="block-home"] > div[class="one"]').each( - function () { - const name = loadedCheerio(this) + loadedCheerio('div[class="block-home"] > div[class="one"]') + .each((index, element) => { + const name = loadedCheerio(element) .find('div[class="img-home"] > a') .attr("title"); - const cover = loadedCheerio(this) + const cover = loadedCheerio(element) .find('div[class="img-home"] > a > img') .attr("src") ?.replace("-150x150", ""); - const url = loadedCheerio(this) + const url = loadedCheerio(element) .find('div[class="img-home"] > a') .attr("href"); if (!name || !url) return; - novels.push({ name, cover, url }); - }, - ); + novels.push({ name, cover, path: url.replace(this.site, "") }); + }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const body = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, name: loadedCheerio('div[class="desc-book"] > h1').text().trim(), cover: loadedCheerio('div[class="img-book"] > img').attr("src"), summary: loadedCheerio('div[id="desc-tab"]').text().trim(), @@ -100,11 +102,11 @@ class Jaomix implements Plugin.PluginBase { if (!name || !url) return; const releaseDate = loadedCheerio(element).find("time").text(); - chapters.push({ - name, - url, - releaseTime: this.parseDate(releaseDate), - chapterNumber: totalChapters - chapterIndex + chapters.push({ + name, + path: url.replace(this.site, ""), + releaseTime: this.parseDate(releaseDate), + chapterNumber: totalChapters - chapterIndex, }); }); @@ -112,8 +114,8 @@ class Jaomix implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); loadedCheerio('div[class="adblock-service"]').remove(); @@ -126,29 +128,30 @@ class Jaomix implements Plugin.PluginBase { searchTerm: string, pageNo: number | undefined = 1, ): Promise { - const url = `${this.site}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd&gpage=${pageNo}`; + const url = + this.site + "/?searchrn=" + searchTerm + + "&but=Поиск по названию&sortby=upd&gpage=" + pageNo; const body = await fetchApi(url).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio('div[class="block-home"] > div[class="one"]').each( - function () { - const name = loadedCheerio(this) + loadedCheerio('div[class="block-home"] > div[class="one"]') + .each((index, element) => { + const name = loadedCheerio(element) .find('div[class="img-home"] > a') .attr("title"); - const cover = loadedCheerio(this) + const cover = loadedCheerio(element) .find('div[class="img-home"] > a > img') .attr("src") ?.replace("-150x150", ""); - const url = loadedCheerio(this) + const url = loadedCheerio(element) .find('div[class="img-home"] > a') .attr("href"); if (!name || !url) return; - novels.push({ name, cover, url }); - }, - ); + novels.push({ name, cover, path: url.replace(this.site, "") }); + }); return novels; } diff --git a/plugins/russian/novelOvh.ts b/plugins/russian/novelOvh.ts index 22463c2..1480cd7 100644 --- a/plugins/russian/novelOvh.ts +++ b/plugins/russian/novelOvh.ts @@ -9,7 +9,7 @@ class novelOvh implements Plugin.PluginBase { name = "НовелОВХ"; site = "https://novel.ovh"; version = "1.0.0"; - icon = "src/ru/novelOvh/icon.png"; + icon = "src/ru/novelovh/icon.png"; async popularNovels( pageNo: number, @@ -27,21 +27,21 @@ class novelOvh implements Plugin.PluginBase { novels.push({ name: novel.name.ru, cover: novel.poster, - url: this.site + "/novel/" + novel.slug, + path: "/novel/" + novel.slug, }), ); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { + async parseNovel(novelPath: string): Promise { const result = await fetchApi( - novelUrl + "?_data=routes/reader/book/$slug/index", + this.site + novelPath + "?_data=routes/reader/book/$slug/index", ); const json = (await result.json()) as responseNovel; const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, name: json.book.name.ru, cover: json.book.poster, genres: json.book.labels?.map?.((label) => label.name).join(","), @@ -67,10 +67,10 @@ class novelOvh implements Plugin.PluginBase { chapters.push({ name: chapter.title || - "Том " + (chapter.volume || 0) + " " + - (chapter.name || "Глава " + - (chapter.number || json.chapters.length - chapterIndex)), - url: novelUrl + "/" + chapter.id + "/0", + "Том " + (chapter.volume || 0) + " " + + (chapter.name || "Глава " + + (chapter.number || json.chapters.length - chapterIndex)), + path: novelPath + "/" + chapter.id + "/0", releaseTime: dayjs(chapter.createdAt).format("LLL"), chapterNumber: json.chapters.length - chapterIndex, }), @@ -80,9 +80,9 @@ class novelOvh implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { + async parseChapter(chapterPath: string): Promise { const result = await fetchApi( - "https://api.novel.ovh/v2/chapters/" + chapterUrl.split("/")[5], + "https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[3], ); const book = (await result.json()) as responseChapter; const image = Object.fromEntries( @@ -106,7 +106,7 @@ class novelOvh implements Plugin.PluginBase { novels.push({ name: novel.name.ru, cover: novel.poster, - url: this.site + "/novel/" + novel.slug, + path: "/novel/" + novel.slug, }), ); diff --git a/plugins/russian/novelTL.ts b/plugins/russian/novelTL.ts index 1461050..beb2a14 100644 --- a/plugins/russian/novelTL.ts +++ b/plugins/russian/novelTL.ts @@ -6,6 +6,13 @@ import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; import dayjs from "dayjs"; +const statusKey: { [key: string]: string } = { + "active": NovelStatus.Ongoing, + "completed": NovelStatus.Completed, + "freezed": NovelStatus.OnHiatus, + "unknown": NovelStatus.Unknown, +}; + class TL implements Plugin.PluginBase { id = "TL"; name = "NovelTL"; @@ -24,9 +31,8 @@ class TL implements Plugin.PluginBase { "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", "Content-Type": "application/json", }, - referrer: this.site, + Referer: this.site, body: JSON.stringify({ - operationName: "Projects", query: "query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}", variables: { @@ -47,7 +53,7 @@ class TL implements Plugin.PluginBase { json.data?.projects?.content?.forEach((novel) => novels.push({ name: novel.title, - url: "https://" + novel.fullUrl, + path: novel.fullUrl, cover: novel?.covers?.[0]?.url ? this.site + novel.covers[0].url : defaultCover, @@ -57,7 +63,7 @@ class TL implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { + async parseNovel(novelPath: string): Promise { const result = await fetchApi(this.site + "/api/site/v2/graphql", { method: "post", headers: { @@ -65,28 +71,25 @@ class TL implements Plugin.PluginBase { "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", "Content-Type": "application/json", }, - referrer: this.site, + Referer: this.site, body: JSON.stringify({ - operationName: "Book", query: 'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus fullUrl covers{url}persons(langs:["ru","en","*"],roles:["author","illustrator"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{shortName chapters{title publishDate fullUrl published}}}}}}}', variables: { - url: novelUrl, + url: novelPath, }, }), }); const json = (await result.json()) as response; const novel: Plugin.SourceNovel = { - url: novelUrl, - name: json.data.project?.title, + path: novelPath, + name: json.data.project?.title || "", cover: json.data.project?.covers?.[0]?.url ? this.site + json.data.project.covers[0].url : defaultCover, summary: json.data.project?.annotation?.text, status: - json.data.project?.translationStatus === "active" - ? NovelStatus.Ongoing - : NovelStatus.Completed, + statusKey[json.data.project?.translationStatus || "unknown"] || NovelStatus.Unknown, }; const genres = [json.data.project?.tags, json.data.project?.genres] @@ -119,9 +122,10 @@ class TL implements Plugin.PluginBase { name: (volume.shortName || "Том " + (volumeIndex + 1)) + " " + (chapter.title || "Глава " + (chapterIndex + 1)), - url: "https://" + chapter.fullUrl, + path: chapter.fullUrl, releaseTime: dayjs(chapter.publishDate).format("LLL"), chapterNumber: chapters.length + 1, + page: work.title || "Основная серия", }); } }), @@ -132,7 +136,7 @@ class TL implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { + async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + "/api/site/v2/graphql", { method: "post", headers: { @@ -140,12 +144,12 @@ class TL implements Plugin.PluginBase { "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", "Content-Type": "application/json", }, - referrer: this.site, + Referer: this.site, body: JSON.stringify({ query: "query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}", variables: { - url: decodeURI(chapterUrl), + url: decodeURI(chapterPath), }, }), }); @@ -177,9 +181,8 @@ class TL implements Plugin.PluginBase { "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", "Content-Type": "application/json", }, - referrer: this.site, + Referer: this.site, body: JSON.stringify({ - operationName: "Projects", query: "query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}", variables: { @@ -198,7 +201,7 @@ class TL implements Plugin.PluginBase { json?.data?.projects?.content?.forEach((novel) => novels.push({ name: novel.title, - url: "https://" + novel.fullUrl, + path: novel.fullUrl, cover: novel?.covers?.[0]?.url ? this.site + novel.covers[0].url : defaultCover, diff --git a/plugins/russian/ranobehub.ts b/plugins/russian/ranobehub.ts index a99a4ef..0b3b5c8 100644 --- a/plugins/russian/ranobehub.ts +++ b/plugins/russian/ranobehub.ts @@ -5,6 +5,12 @@ import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; import dayjs from "dayjs"; +const statusKey: { [key: number]: string } = { + 1: NovelStatus.Ongoing, + 2: NovelStatus.Completed, + 3: NovelStatus.OnHiatus, +}; + class RNBH implements Plugin.PluginBase { id = "RNBH.org"; name = "RanobeHub"; @@ -14,10 +20,13 @@ class RNBH implements Plugin.PluginBase { async popularNovels( pageNo: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + "/api/search?page=" + pageNo + "&sort="; - url += showLatestNovels + url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "computed_rating"; url += "&status=" + (filters?.status?.value ? filters?.status?.value : "0"); @@ -30,7 +39,9 @@ class RNBH implements Plugin.PluginBase { const includeTags = [ filters.tags?.value?.include, filters.events?.value?.include, - ].flat().filter((t) => t); + ] + .flat() + .filter((t) => t); if (includeTags.length) { url += "&tags:positive=" + includeTags.join(","); @@ -39,7 +50,9 @@ class RNBH implements Plugin.PluginBase { const excludeTags = [ filters.tags?.value?.exclude, filters.events?.value?.exclude, - ].flat().filter((t) => t); + ] + .flat() + .filter((t) => t); if (excludeTags.length) { url += "&tags:negative=" + excludeTags.join(","); @@ -54,29 +67,24 @@ class RNBH implements Plugin.PluginBase { novels.push({ name: novel.names.rus || novel.names.eng || novel.names.original, cover: novel.poster.medium, - url: novel.url, + path: "/ranobe/" + novel.id, }), ); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const novelId = novelUrl - .substring("https://ranobehub.org/ranobe/".length) - .split("-")[0]; - const result = await fetchApi(this.site + "/api/ranobe/" + novelId); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + "/api" + novelPath); const json = (await result.json()) as { data: responseNovel }; const novel: Plugin.SourceNovel = { - url: novelUrl, - name: json.data.names?.rus || json.data.names.eng, + path: novelPath, + name: json.data.names.rus || json.data.names.eng || "", cover: json.data.posters.medium, - summary: json.data.description, + summary: json.data.description.trim(), author: json.data?.authors?.[0]?.name_eng || "", - status: json.data.status.title.includes("процессе") - ? NovelStatus.Ongoing - : NovelStatus.Completed, + status: statusKey[json.data.status.id] || NovelStatus.Unknown, }; const tags = [json.data.tags.events, json.data.tags.genres] @@ -89,28 +97,33 @@ class RNBH implements Plugin.PluginBase { } const chapters: Plugin.ChapterItem[] = []; - const chaptersRaw = await fetchApi(`${this.site}/api/ranobe/${novelId}/contents`); + const chaptersRaw = await fetchApi( + this.site + "/api" + novelPath + "/contents", + ); const chaptersJSON = (await chaptersRaw.json()) as { volumes: VolumesEntity[]; }; chaptersJSON.volumes.forEach((volume) => - volume.chapters?.forEach((chapter) => { - chapters.push({ - name: chapter.name, - url: chapter.url, - releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format("LLL"), - chapterNumber: chapters.length + 1, - }); - }), + volume.chapters?.forEach((chapter) => + chapters.push({ + name: chapter.name, + path: + volume.num && chapter.num + ? novelPath + "/" + volume.num + "/" + chapter.num + : chapter.url.replace(this.site, ""), + releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format("LLL"), + chapterNumber: chapters.length + 1, + }) + ) ); novel.chapters = chapters; return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -132,10 +145,7 @@ class RNBH implements Plugin.PluginBase { return chapterText; } - async searchNovels( - searchTerm: string, - //pageNo: number | undefined = 1, - ): Promise { + async searchNovels(searchTerm: string): Promise { const url = `${this.site}/api/fulltext/global?query=${searchTerm}&take=10`; const result = await fetchApi(url); const data = (await result.json()) as responseSearch[]; @@ -151,11 +161,7 @@ class RNBH implements Plugin.PluginBase { novel.name || novel?.names?.original || "", - url: - "https://ranobehub.org/ranobe/" + - novel?.url?.match( - /https:\/\/ranobehub\.org\/ranobe\/(.*?)\?utm_source=search_name&utm_medium=search&utm_campaign=search_using/, - )?.[1], + path: "/ranobe/" + novel.id, cover: novel?.image?.replace("/small", "/medium"), }), ); diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index e64374e..4713d04 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -5,12 +5,12 @@ import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; import dayjs from "dayjs"; -const statusKey = [ - NovelStatus.Ongoing, - NovelStatus.Completed, - NovelStatus.OnHiatus, - NovelStatus.Cancelled, -]; +const statusKey: { [key: number]: string } = { + 1: NovelStatus.Ongoing, + 2: NovelStatus.Completed, + 3: NovelStatus.OnHiatus, + 4: NovelStatus.Cancelled, +}; class RLIB implements Plugin.PluginBase { id = "RLIB"; @@ -22,22 +22,26 @@ class RLIB implements Plugin.PluginBase { async popularNovels( pageNo: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + "/manga-list?sort="; url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate"; url += "&dir=" + (filters?.order?.value || "desc"); + url += "&chapters[min]=" + (filters?.require_chapters?.value ? "1" : "0"); Object.entries(filters || {}).forEach(([type, { value }]: any) => { if (value instanceof Array && value.length) { url += "&" + type + "[]=" + value.join("&" + type + "[]="); } if (value?.include instanceof Array && value.include.length) { - url += "&" + type + "[include][]=" + + url += "&" + type + "[include][]=" + value.include.join("&" + type + "[include][]="); - } + } if (value?.exclude instanceof Array && value.exclude.length) { - url += "&" + type + "[exclude][]=" + + url += "&" + type + "[exclude][]=" + value.exclude.join("&" + type + "[exclude][]="); } }); @@ -51,27 +55,25 @@ class RLIB implements Plugin.PluginBase { ?.replace?.(/[^0-9]/g, ""); const novels: Plugin.NovelItem[] = []; - loadedCheerio(".media-card-wrap").each(function () { - const name = loadedCheerio(this).find(".media-card__title").text(); - const cover = loadedCheerio(this).find("a.media-card").attr("data-src"); - const url = loadedCheerio(this).find("a.media-card").attr("href"); + loadedCheerio(".media-card-wrap").each((index, element) => { + const name = loadedCheerio(element).find(".media-card__title").text(); + const cover = loadedCheerio(element).find("a.media-card").attr("data-src"); + const url = loadedCheerio(element).find("a.media-card").attr("href"); if (!url) return; - novels.push({ name, cover, url }); + novels.push({ name, cover, path: url.replace(this.site, "") }); }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const body = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, + name: loadedCheerio(".media-name__main").text()?.trim?.() || "", }; - - novel.name = loadedCheerio(".media-name__main").text().trim(); - novel.cover = loadedCheerio(".container_responsive img").attr("src"); novel.summary = loadedCheerio(".media-description__text").text().trim(); @@ -102,45 +104,75 @@ class RLIB implements Plugin.PluginBase { } }); - const chapters: Plugin.ChapterItem[] = []; + const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/); + if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) { + const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]); - let chaptersRaw: any = body.match( - /window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm, - ); + if (!novel.name) { + novel.name = + chaptersJson.manga.rusName || + chaptersJson.manga.engName || + chaptersJson.manga.name; + } + novel.status = statusKey[chaptersJson.manga.status] || NovelStatus.Unknown; + this.ui = chaptersJson?.user?.id; - chaptersRaw = chaptersRaw?.[0] - ?.replace("window.__DATA__ = ", "") - ?.replace("window._SITE_COLOR_", "") - ?.trim() - ?.slice(0, -1); + if (!chaptersJson.chapters.list?.length) return novel; + const totalChapters = chaptersJson.chapters.list.length; - const chaptersJson: responseBook = JSON.parse(chaptersRaw); - const totalChapters = chaptersJson.chapters.list?.length || 0; - novel.status = statusKey[chaptersJson.manga.status - 1] || NovelStatus.Unknown; - this.ui = chaptersJson?.user?.id; + const customPage: { [key: number]: string } = {}; + const customOrder: { [key: number]: number } = {}; + if (chaptersJson.chapters.branches?.length && chaptersJson.chapters.branches.length > 1) { + //if the novel is being translated by more than one team + chaptersJson.chapters.branches.forEach(({ teams, id }) => { + if (teams?.length) { + customPage[id || 0] = + teams.find((team) => team.is_active)?.name || teams[0].name; + } + }); + //fixes the chapter's position. + chaptersJson.chapters.list.forEach((chapter) => { + chapter.index = customOrder[chapter.branch_id || 0] || 1; + customOrder[chapter.branch_id || 0] = + (customOrder[chapter.branch_id || 0] || 1) + 1; + }); + }; - chaptersJson.chapters?.list?.forEach((chapter, chapterIndex) => - chapters.push({ - name: - "Том " + chapter.chapter_volume + - " Глава " + chapter.chapter_number + - (chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""), - url: - this.site + "/" + chaptersJson.manga.slug + - "/v" + chapter.chapter_volume + "/c" + chapter.chapter_number + - "?bid=" + (chapter.branch_id || ""), - releaseTime: dayjs(chapter.chapter_created_at).format("LLL"), - chapterNumber: totalChapters - chapterIndex, - }), - ); - - novel.chapters = chapters.reverse(); + const chapters: Plugin.ChapterItem[] = []; + chaptersJson.chapters.list.forEach((chapter, chapterIndex) => + chapters.push({ + name: + "Том " + chapter.chapter_volume + + " Глава " + chapter.chapter_number + + (chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""), + path: + "/" + chaptersJson.manga.slug + + "/v" + chapter.chapter_volume + + "/c" + chapter.chapter_number + + "?bid=" + (chapter.branch_id || ""), + releaseTime: dayjs(chapter.chapter_created_at).format("LLL"), + chapterNumber: + customOrder[chapter.branch_id || 0] - (chapter.index || 0) || + totalChapters - chapterIndex, + page: customPage[chapter.branch_id || 0] || "Основной перевод", + }), + ); + novel.chapters = + chaptersJson.chapters.branches?.length && + chaptersJson.chapters.branches.length > 1 + ? chapters.sort((a, b) => { + if ((a.page || 0) > (b.page || 0)) return 1; + if ((a.page || 0) < (b.page || 0)) return -1; + return (a.chapterNumber || 0) - (b.chapterNumber || 0); + }) + : chapters.reverse(); + } return novel; } - async parseChapter(chapterUrl: string): Promise { + async parseChapter(chapterPath: string): Promise { const result = await fetchApi( - chapterUrl + (this.ui ? "&ui=" + this.ui : ""), + this.site + chapterPath + (this.ui ? "&ui=" + this.ui : ""), ).then((res) => res.text()); const loadedCheerio = parseHTML(result); @@ -160,21 +192,18 @@ class RLIB implements Plugin.PluginBase { return chapterText || ""; } - async searchNovels( - searchTerm: string, - //pageNo: number | undefined = 1, - ): Promise { + async searchNovels(searchTerm: string): Promise { const result = await fetchApi( - `${this.site}/search?q=${searchTerm}&type=manga`, + this.site + "/search?q=" + searchTerm + "&type=manga", ); const body = (await result.json()) as Manga[]; const novels: Plugin.NovelItem[] = []; body.forEach((novel) => novels.push({ - name: novel?.rus_name || novel.name, - cover: novel?.coverImage, - url: novel?.href || this.site + "/" + novel.slug, + name: novel.rus_name || novel.name, + cover: novel.coverImage, + path: "/" + novel.slug, }), ); @@ -396,6 +425,11 @@ class RLIB implements Plugin.PluginBase { ], type: FilterTypes.ExcludableCheckboxGroup, }, + require_chapters: { + label: "Только проекты с главами", + value: true, + type: FilterTypes.Switch, + }, } satisfies Filters; } @@ -424,12 +458,13 @@ interface Manga { href?: string; } interface Chapters { - list?: ListEntity[] | null; - teams?: TeamsEntity[] | null; - branches?: BranchesEntity[] | null; - is_paid?: null[] | null; + list?: ListEntity[]; + teams?: TeamsEntity[]; + branches?: BranchesEntity[]; + is_paid?: string[] | null; } interface ListEntity { + index?: number; //crutch chapter_id: number; chapter_slug: string; chapter_name: string; diff --git a/plugins/russian/ranoberf.ts b/plugins/russian/ranoberf.ts index 494ff75..1b6d980 100644 --- a/plugins/russian/ranoberf.ts +++ b/plugins/russian/ranoberf.ts @@ -37,15 +37,15 @@ class RNRF implements Plugin.PluginBase { cover: novel?.verticalImage?.url ? this.site + novel.verticalImage.url : defaultCover, - url: this.site + "/" + novel.slug, + path: "/" + novel.slug, }), ); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const result = await fetchApi(novelUrl); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -53,8 +53,8 @@ class RNRF implements Plugin.PluginBase { const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book; const novel: Plugin.SourceNovel = { - url: novelUrl, - name: book?.title, + path: novelPath, + name: book?.title || "", cover: book?.verticalImage?.url ? this.site + book.verticalImage.url : defaultCover, @@ -72,7 +72,7 @@ class RNRF implements Plugin.PluginBase { if (!chapter.isDonate || chapter.isUserPaid) { chapters.push({ name: chapter.title, - url: this.site + chapter.url, + path: chapter.url, releaseTime: dayjs(chapter.publishedAt).format("LLL"), chapterNumber: book.chapters.length - chapterIndex, }); @@ -83,8 +83,10 @@ class RNRF implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath).then((res) => + res.text(), + ); let loadedCheerio = parseHTML(result); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); @@ -105,10 +107,7 @@ class RNRF implements Plugin.PluginBase { return chapterText; } - async searchNovels( - searchTerm: string, - //pageNo: number | undefined = 1, - ): Promise { + async searchNovels(searchTerm: string): Promise { const url = `${this.site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`; const result = await fetchApi(url); const body = (await result.json()) as { items: Item[] }; @@ -120,12 +119,13 @@ class RNRF implements Plugin.PluginBase { cover: novel?.verticalImage?.url ? this.site + novel.verticalImage.url : defaultCover, - url: this.site + "/" + novel.slug, + path: "/" + novel.slug, }), ); return novels; } + fetchImage = fetchFile; filters = { diff --git a/plugins/russian/renovels.ts b/plugins/russian/renovels.ts index 1cffaf6..c64b0f9 100644 --- a/plugins/russian/renovels.ts +++ b/plugins/russian/renovels.ts @@ -4,32 +4,34 @@ import { fetchApi, fetchFile } from "@libs/fetch"; import { NovelStatus } from "@libs/novelStatus"; import dayjs from "dayjs"; +const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); + class ReN implements Plugin.PluginBase { id = "ReN"; name = "Renovels"; + icon = "src/ru/renovels/icon.png"; site = "https://renovels.org"; version = "1.0.0"; - icon = "src/ru/renovels/icon.png"; async popularNovels( pageNo: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/api/search/catalog/?count=30&ordering="; - url += filters?.order?.value ? "-" : ""; + let url = this.site + "/api/search/catalog/?count=30&ordering=-"; url += showLatestNovels ? "chapter_date" : filters?.sort?.value || "rating"; Object.entries(filters || {}).forEach(([type, { value }]: any) => { if (value instanceof Array && value.length) { - url += "&" + type + "=" + - value.join("&" + type + "="); + url += "&" + type + "=" + value.join("&" + type + "="); } if (value?.include instanceof Array && value.include.length) { - url += "&" + type + "=" + - value.include.join("&" + type + "="); + url += "&" + type + "=" + value.include.join("&" + type + "="); } if (value?.exclude instanceof Array && value.exclude.length) { - url += "&exclude_" + type + "=" + + url += "&exclude_" + type + "=" + value.exclude.join("&exclude_" + type + "="); } }); @@ -42,26 +44,27 @@ class ReN implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = body.content.map((novel) => ({ name: novel.main_name || novel.secondary_name, cover: this.site + (novel.img.high || novel.img.mid || novel.img.low), - url: this.site + "/novel/" + novel.dir, + path: "/novel/" + novel.dir, })); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const novelID = novelUrl.split("/")[4]; + async parseNovel(novelPath: string): Promise { + const novelID = novelPath.split("/")[2]; const result = await fetchApi(this.site + "/api/titles/" + novelID); const body = (await result.json()) as { content: responseNovel }; const novel: Plugin.SourceNovel = { - url: novelUrl, - name: body.content.main_name || body.content.secondary_name || body.content.another_name, + path: novelPath, + name: + body.content.main_name || + body.content.secondary_name || + body.content.another_name, summary: body.content.description, cover: this.site + - (body.content.img.high || - body.content.img.mid || - body.content.img.low), + (body.content.img.high || body.content.img.mid || body.content.img.low), status: body.content.status.name === "Продолжается" ? NovelStatus.Ongoing @@ -77,15 +80,18 @@ class ReN implements Plugin.PluginBase { novel.genres = tags.join(","); } - const all = body.content.count_chapters / 100 + 1; + const totalPages = + (body.content.branches?.[0]?.count_chapters || + body.content.count_chapters) / 100; const branch_id = body.content.branches?.[0]?.id || body.content.id; const chapters: Plugin.ChapterItem[] = []; - for (let i = 0; i < all; i++) { + for (let page = 0; page < totalPages; page++) { + await delay(1000); const chapterResult = await fetchApi( this.site + "/api/titles/chapters/?branch_id=" + branch_id + - "&count=100&page=" + (i + 1), + "&count=100&page=" + (page + 1), ); const volumes = (await chapterResult.json()) as { content: responseСhapters[]; @@ -95,10 +101,10 @@ class ReN implements Plugin.PluginBase { if (!chapter.is_paid || chapter.is_bought) { chapters.push({ name: - "Том " + chapter.tome + - " Глава " + chapter.chapter + + "Том " + chapter.tome + + " Глава " + chapter.chapter + (chapter.name ? " " + chapter.name.trim() : ""), - url: `${this.site}/novel/${novelID}/${chapter.id}/`, + path: "/novel/" + novelID + "/" + chapter.id, releaseTime: dayjs(chapter.upload_date).format("LLL"), chapterNumber: chapter.index, }); @@ -110,8 +116,8 @@ class ReN implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const url = this.site + "/api/titles/chapters/" + chapterUrl.split("/")[5]; + async parseChapter(chapterPath: string): Promise { + const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[3]; const result = await fetchApi(url); const body = (await result.json()) as { content: responseСhapter }; @@ -131,7 +137,7 @@ class ReN implements Plugin.PluginBase { novels.push({ name: novel.main_name || novel.secondary_name, cover: this.site + (novel.img.high || novel.img.mid || novel.img.low), - url: this.site + "/novel/" + novel.dir, + path: "/novel/" + novel.dir, }), ); @@ -154,15 +160,6 @@ class ReN implements Plugin.PluginBase { ], type: FilterTypes.Picker, }, - order: { - label: "Порядок", - value: "-", - options: [ - { label: "По убыванию", value: "-" }, - { label: "По возрастанию", value: "" }, - ], - type: FilterTypes.Picker, - }, genres: { label: "Жанры:", value: { include: [], exclude: [] }, diff --git a/plugins/spanish/hasutl.ts b/plugins/spanish/hasutl.broken.ts similarity index 100% rename from plugins/spanish/hasutl.ts rename to plugins/spanish/hasutl.broken.ts diff --git a/plugins/spanish/novelasligera.ts b/plugins/spanish/novelasligera.broken.ts similarity index 100% rename from plugins/spanish/novelasligera.ts rename to plugins/spanish/novelasligera.broken.ts diff --git a/plugins/spanish/novelawuxia.ts b/plugins/spanish/novelawuxia.broken.ts similarity index 100% rename from plugins/spanish/novelawuxia.ts rename to plugins/spanish/novelawuxia.broken.ts diff --git a/plugins/spanish/oasistranslations.ts b/plugins/spanish/oasistranslations.broken.ts similarity index 100% rename from plugins/spanish/oasistranslations.ts rename to plugins/spanish/oasistranslations.broken.ts diff --git a/plugins/spanish/skynovels.ts b/plugins/spanish/skynovels.broken.ts similarity index 100% rename from plugins/spanish/skynovels.ts rename to plugins/spanish/skynovels.broken.ts diff --git a/plugins/spanish/tunovelaligera.ts b/plugins/spanish/tunovelaligera.ts index 48f2508..5a6442f 100644 --- a/plugins/spanish/tunovelaligera.ts +++ b/plugins/spanish/tunovelaligera.ts @@ -1,17 +1,20 @@ -import { load as parseHTML } from "cheerio"; +import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { FilterTypes, Filters } from "@libs/filterInputs"; import { Plugin } from "@typings/plugin"; import { defaultCover } from "@libs/defaultCover"; import { NovelStatus } from "@libs/novelStatus"; -class TuNovelaLigera implements Plugin.PluginBase { +class TuNovelaLigera implements Plugin.PagePlugin { id = "tunovelaligera"; name = "TuNovelaLigera"; icon = "src/es/tunovelaligera/icon.png"; site = "https://tunovelaligera.com"; version = "1.0.0"; + async sleep (ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) + } async popularNovels( pageNo: number, { @@ -36,27 +39,51 @@ class TuNovelaLigera implements Plugin.PluginBase { const url = loadedCheerio(el).find(".h5 > a").attr("href"); if (!url) return; - novels.push({ name, cover, url }); + novels.push({ name, cover, path: url.replace(this.site, '') }); }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { + parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio("#lcp_instance_0 li").each((i, el) => { + const chapterName = loadedCheerio(el) + .find("a") + .text() + .replace(/[\t\n]/g, "") + .trim(); + + const chapterUrl = loadedCheerio(el).find("a").attr("href"); + if(!chapterUrl) return; + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + return chapters; + } + + async parseNovel(novelPath: string): Promise { + const novelUrl = this.site + novelPath; const result = await fetchApi(novelUrl); const body = await result.text(); let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url: novelUrl, + let lastPage = 1; + loadedCheerio('ul.lcp_paginator > li > a').each(function() { + const page = Number(this.attribs['title']); + if(page && page > lastPage) lastPage = page; + }); + const novel: Plugin.SourceNovel & { totalPages: number; } = { + path: novelPath, chapters: [], + totalPages: lastPage, + name: loadedCheerio(".post-title > h1").text().trim() }; loadedCheerio(".manga-title-badges").remove(); - novel.name = loadedCheerio(".post-title > h1").text().trim(); - let novelCover = loadedCheerio(".summary_image > a > img"); novel.cover = @@ -90,95 +117,43 @@ class TuNovelaLigera implements Plugin.PluginBase { novel.summary = loadedCheerio("div.summary__content > p").text().trim(); - let chapter: Plugin.ChapterItem[] = []; - - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - let lastPage = 1; - lastPage = +loadedCheerio(".lcp_paginator li:last").prev().text(); - - const getChapters = async () => { - const n = novelUrl.split("/"); - const novelName = n[4]; - const formData = new FormData(); - formData.append("action", "madara_load_more"); - formData.append("page", "0"); - formData.append("template", "html/loop/content"); - formData.append("vars[category_name]", novelName); - formData.append("vars[posts_per_page]", "10000"); - - const result = await fetchApi(this.site + '/wp-admin/admin-ajax.php', { - method: "POST", - body: formData, - }); - const text = await result.text(); - - loadedCheerio = parseHTML(text); - - loadedCheerio(".heading").each((i, el) => { - const chapterName = loadedCheerio(el) - .text() - .replace(/[\t\n]/g, "") - .trim(); - const releaseDate = null; - const chapterUrl = loadedCheerio(el).find("a").attr("href") || ""; - - chapter.push({ - name: chapterName, - url: chapterUrl, - releaseTime: releaseDate, - }); - }); - return chapter.reverse(); - }; - - const getPageChapters = async () => { - for (let i = 1; i <= lastPage; i++) { - const chaptersUrl = `${novelUrl}?lcp_page0=${i}`; - const result = await fetchApi(chaptersUrl); - const chaptersHTML = await result.text(); - - let chapterCheerio = parseHTML(chaptersHTML); - - chapterCheerio("#lcp_instance_0 li").each((i, el) => { - const chapterName = chapterCheerio(el) - .find("a") - .text() - .replace(/[\t\n]/g, "") - .trim(); - - const releaseDate = chapterCheerio(el).find("span").text().trim(); - - const chapterUrl = chapterCheerio(el).find("a").attr("href") || ""; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - await delay(1000); - } - return chapter.reverse(); - }; - - novel.chapters = await getChapters(); - - if (!novel.chapters.length) { - await delay(1000); - novel.chapters = await getPageChapters(); - } + novel.chapters = this.parseChapters(loadedCheerio); return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); + async parsePage(novelPath: string, page: string): Promise { + const novelUrl = this.site + novelPath; + const pageUrl = this.site + novelPath + '?lcp_page0=' + page; + const result = await fetchApi(novelUrl); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + const latestChapterEle = loadedCheerio('#lcp_instance_0 li').first(); + const latestChapterUrl = loadedCheerio(latestChapterEle).find("a").attr("href"); + const latestChapterName = loadedCheerio(latestChapterEle).find("a") + .text() + .replace(/[\t\n]/g, "") + .trim(); + const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? { + path: latestChapterUrl.replace(this.site, ''), + name: latestChapterName + } : undefined + await this.sleep(1000); + const pageText = await fetchApi(pageUrl).then(res => res.text()); + const chapters = this.parseChapters(parseHTML(pageText)); + return { + chapters, + latestChapter + } + } + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); - loadedCheerio("#hola_siguiente").next().find("div").remove(); - const chapterText = loadedCheerio("#hola_siguiente").next().html() || ""; + const chapterText = loadedCheerio(".c-blog-post.post").html() || ""; return chapterText; } @@ -201,7 +176,7 @@ class TuNovelaLigera implements Plugin.PluginBase { const url = loadedCheerio(el).find(".h4 > a").attr("href"); if (!url) return; - novels.push({ name, cover, url }); + novels.push({ name, cover, path: url.replace(this.site, '') }); }); return novels; diff --git a/plugins/spanish/yukitls.ts b/plugins/spanish/yukitls.broken.ts similarity index 100% rename from plugins/spanish/yukitls.ts rename to plugins/spanish/yukitls.broken.ts diff --git a/plugins/turkish/epiknovel.ts b/plugins/turkish/epiknovel.broken.ts similarity index 100% rename from plugins/turkish/epiknovel.ts rename to plugins/turkish/epiknovel.broken.ts diff --git a/plugins/turkish/lshnovel.ts b/plugins/turkish/lshnovel.broken.ts similarity index 100% rename from plugins/turkish/lshnovel.ts rename to plugins/turkish/lshnovel.broken.ts diff --git a/plugins/ukrainian/smakolykytl.ts b/plugins/ukrainian/smakolykytl.ts index 22b7d97..cb14b6b 100644 --- a/plugins/ukrainian/smakolykytl.ts +++ b/plugins/ukrainian/smakolykytl.ts @@ -10,8 +10,6 @@ class Smakolykytl implements Plugin.PluginBase { version = "1.0.0"; icon = "src/uk/smakolykytl/icon.png"; - - async popularNovels( pageNo: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions, @@ -28,23 +26,23 @@ class Smakolykytl implements Plugin.PluginBase { novels.push({ name: novel.title, cover: novel.image.url, - url: this.site + "titles/" + novel.id, + path: "titles/" + novel.id, }), ); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const id = novelUrl.split("/").pop(); + async parseNovel(novelPath: string): Promise { + const id = novelPath.split("/").pop(); const result = await fetchApi( "https://api.smakolykytl.site/api/user/projects/" + id, ); const book = (await result.json()) as response; const novel: Plugin.SourceNovel = { - url: novelUrl, - name: book?.project?.title, + path: novelPath, + name: book?.project?.title || "", cover: book?.project?.image?.url, summary: book?.project?.description, author: book?.project?.author, @@ -67,22 +65,22 @@ class Smakolykytl implements Plugin.PluginBase { ); const data = (await res.json()) as response; data?.books?.forEach((volume) => - volume?.chapters?.forEach((chapter) => - chapters.push({ - name: volume.title + " " + chapter.title, - url: this.site + "read/" + chapter.id, - releaseTime: dayjs(chapter.modifiedAt).format("LLL"), - chapterNumber: chapters.length + 1, - }), - ), + volume?.chapters?.forEach((chapter) => + chapters.push({ + name: volume.title + " " + chapter.title, + path: "read/" + chapter.id, + releaseTime: dayjs(chapter.modifiedAt).format("LLL"), + chapterNumber: chapters.length + 1, + }), + ), ); novel.chapters = chapters; return novel; } - async parseChapter(chapterUrl: string): Promise { - const id = chapterUrl.split("/").pop(); + async parseChapter(chapterPath: string): Promise { + const id = chapterPath.split("/").pop(); const result = await fetchApi( "https://api.smakolykytl.site/api/user/chapters/" + id, ); @@ -93,26 +91,22 @@ class Smakolykytl implements Plugin.PluginBase { return chapterText; } - async searchNovels( - searchTerm: string, - //pageNo: number | undefined = 1, - ): Promise { - const result = await fetchApi( - "https://api.smakolykytl.site/api/user/projects", - ); + async searchNovels(searchTerm: string): Promise { + const result = await fetchApi("https://api.smakolykytl.site/api/user/projects"); const json = (await result.json()) as response; + const searchTitle = searchTerm.toLowerCase(); const novels: Plugin.NovelItem[] = []; - + json?.projects ?.filter( - (novel) => - novel.title.includes(searchTerm) || String(novel.id) === searchTerm, + ({ title, id }) => + title.toLowerCase().includes(searchTitle) || String(id) == searchTerm, ) ?.forEach((novel) => novels.push({ name: novel.title, cover: novel.image.url, - url: this.site + "titles/" + novel.id, + path: "titles/" + novel.id, }), ); return novels; diff --git a/plugins/vietnamese/LNHako.ts b/plugins/vietnamese/LNHako.ts index 3e3c658..f18ab4d 100644 --- a/plugins/vietnamese/LNHako.ts +++ b/plugins/vietnamese/LNHako.ts @@ -18,10 +18,6 @@ class HakoPlugin implements Plugin.PluginBase { .find("div.thumb_attr.series-title > a") .attr("href"); - if (url && !isUrlAbsolute(url)) { - url = this.site + url; - } - if (url) { const name = loadedCheerio(ele) .find(".series-title") @@ -35,7 +31,7 @@ class HakoPlugin implements Plugin.PluginBase { cover = this.site + cover; } - const novel = { name, url, cover }; + const novel = { name, path: url.replace(this.site, ''), cover }; novels.push(novel); } @@ -64,11 +60,12 @@ class HakoPlugin implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { + async parseNovel(novelPath: string): Promise { const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, + name: 'Không có tiêu đề', }; - const result = await fetch(novelUrl); + const result = await fetch(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); @@ -121,39 +118,46 @@ class HakoPlugin implements Plugin.PluginBase { novel.status = NovelStatus.Unknown; } - let num = 0, part = 1; - const chapters: Plugin.ChapterItem[] = loadedCheerio('.list-chapters li').toArray().map((ele) => { - let chapterUrl = this.site + loadedCheerio(ele).find("a").attr("href"); - const chapterName = loadedCheerio(ele) - .find(".chapter-name") - .text() - .trim(); - let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]); - if(chapterNumber){ - num = chapterNumber; - part = 1; - }else{ - chapterNumber = num + part / 10; - part ++; - } - const chapterTime = loadedCheerio(ele) - .find(".chapter-time") - .text() - .split('/') - .map(x => Number(x)); - return { - url: chapterUrl || '', - name: chapterName, - releaseTime: new Date(chapterTime[2], chapterTime[1], chapterTime[0]).toISOString(), - chapterNumber: chapterNumber, - }; - }).filter(c => c.url); - - novel.chapters = chapters; + novel.chapters = []; + loadedCheerio('.volume-list').each((idx, ele) => { + const customPage = loadedCheerio(ele).find('.sect-title').text().trim(); + let num = 0, part = 1; + loadedCheerio(ele).find('.list-chapters > li').each((idx, chapterEle) => { + const path = loadedCheerio(chapterEle).find("a").attr("href"); + const chapterName = loadedCheerio(chapterEle) + .find(".chapter-name") + .text() + .trim(); + let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]); + if(chapterNumber){ + num = chapterNumber; + part = 1; + }else{ + chapterNumber = num + part / 10; + part ++; + } + const chapterTime = loadedCheerio(chapterEle) + .find(".chapter-time") + .text() + .split('/') + .map(x => Number(x)); + if(path){ + novel.chapters?.push( + { + page: customPage, + path, + name: chapterName, + releaseTime: new Date(chapterTime[2], chapterTime[1], chapterTime[0]).toISOString(), + chapterNumber: chapterNumber, + } + ); + } + }); + }); return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetch(chapterUrl); + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); diff --git a/plugins/vietnamese/Truyenconect.broken.ts b/plugins/vietnamese/Truyenconect.broken.ts deleted file mode 100644 index 692e220..0000000 --- a/plugins/vietnamese/Truyenconect.broken.ts +++ /dev/null @@ -1,228 +0,0 @@ - -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; - -class TruyenConect implements Plugin.PluginBase { - id = "truyenconect"; - name = "Truyen Conect"; - icon = "src/vi/truyenconect/icon.png"; - site = "https://truyenconect.com"; - version = "1.0.0"; - parseNovels(loadedCheerio: CheerioAPI, selector: string){ - const novels: Plugin.NovelItem[] = []; - loadedCheerio(selector).each(function () { - const url = loadedCheerio(this).find('a').attr('href'); - if(url){ - novels.push({ - name: loadedCheerio(this).find('img').attr('alt') || '', - url, - cover: loadedCheerio(this).find('img').attr('data-src'), - }) - } - }); - return novels; - } - async popularNovels(pageNo: number, {filters, showLatestNovels}: Plugin.PopularNovelsOptions): Promise { - let link = this.site; - let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover'; - - if(showLatestNovels){ - selector = '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover'; - } - - if(filters.category.value){ - link += '/' + filters.category.value; - selector = 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]'; - if(filters.category.value === 'the-loai'){ - selector = '.item-thumb.hover-details.c-image-hover' - link += '/' + filters.genre.value; - } - link += '?page=' + pageNo; - } - - const body = await fetch(link).then(r => r.text()); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio, selector); - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const result = await fetchApi(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio(".post-title > h1").text().trim(); - - novel.cover = loadedCheerio(".summary_image > a > img").attr( - "data-src" - ); - - loadedCheerio(".post-content_item").each(function () { - const detailName = loadedCheerio(this) - .find(".summary-heading > h5") - .text() - .trim(); - const detail = loadedCheerio(this).find(".summary-content").html(); - - if (!detail) return; - - switch (detailName) { - case "Genre": - novel.genres = loadedCheerio(detail) - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - break; - case "Author": - novel.author = loadedCheerio(detail) - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(", "); - break; - case "Novel": - novel.status = detail?.trim().replace(/G/g, "g"); - break; - } - }); - - loadedCheerio( - ".description-summary > div.summary__content > div" - ).remove(); - - novel.summary = loadedCheerio( - ".description-summary > div.summary__content" - ) - .text() - .trim(); - - let chapter: Plugin.ChapterItem[] = []; - loadedCheerio(".wp-manga-chapter").each(function () { - const chapterName = loadedCheerio(this) - .find("a") - .text() - .replace(/[\t\n]/g, "") - .trim(); - const releaseDate = loadedCheerio(this).find("span").text().trim(); - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - loadedCheerio("img").removeAttr("srcset"); - let chapterText = loadedCheerio(".reading-content").html() || ''; - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.site}?s=${searchTerm}&post_type=wp-manga`; - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio(".c-tabs-item__content").each(function () { - const novelName = loadedCheerio(this).find(".h4 > a").text(); - const novelCover = loadedCheerio(this).find("img").attr("data-src"); - const novelUrl = loadedCheerio(this).find(".h4 > a").attr("href"); - - if (!novelUrl) return; - - novels.push({ - name: novelName, - url: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - filters = { - category: { - label: "Lọc theo", - value: '', - type: FilterTypes.Picker, - options: [ - {label: '', value: ''}, - {label: 'Thể loại', value: 'the-loai'}, - {label: 'Truyện dịch', value: 'truyen-dich'}, - {label: 'Truyện convert', value: 'convert'} - ] - }, - genre: { - label: 'Thể loại', - value: 'action', - type: FilterTypes.Picker, - options: [ - {label: 'Action', value: 'action'}, - {label: 'Adult', value: 'adult'}, - {label: 'Adventure', value: 'adventure'}, - {label: 'Chinese novel', value: 'chinese-novel'}, - {label: 'Chuyển Sinh', value: 'chuyen-sinh'}, - {label: 'English Novel', value: 'english-novel'}, - {label: 'Harem', value: 'harem'}, - {label: 'Ecchi', value: 'ecchi'}, - {label: 'Fantasy', value: 'fantasy'}, - {label: 'Drama', value: 'drama'}, - {label: 'Game', value: 'game'}, - {label: 'Tiên hiệp', value: 'tien-hiep'}, - {label: 'Kiếm Hiệp', value: 'kiem-hiep'}, - {label: 'Ngôn Tình', value: 'ngon-tinh'}, - {label: 'Isekai', value: 'isekai'}, - {label: 'Lịch Sử', value: 'lich-su'}, - {label: 'Web Novel', value: 'web-novel'}, - {label: 'Xuyên không', value: 'xuyen-khong'}, - {label: 'Trọng sinh', value: 'trong-sinh'}, - {label: 'Trinh thám', value: 'trinh-tham'}, - {label: 'Dị giới', value: 'di-gioi'}, - {label: 'Huyền ảo', value: 'huyen-ao'}, - {label: 'Sắc Hiệp', value: 'sac-hiep'}, - {label: 'Dị năng', value: 'di-nang'}, - {label: 'Linh dị', value: 'linh-di'}, - {label: 'Đô thị', value: 'do-thi'}, - {label: 'Comedy', value: 'comedy'}, - {label: 'School Life', value: 'school-life'}, - {label: 'Romance', value: 'romance'}, - {label: 'Martial-arts', value: 'martial-arts'}, - {label: 'Light Novel', value: 'light-novel'}, - {label: 'Huyền huyễn', value: 'huyen-huyen'}, - {label: 'Kỳ Huyễn', value: 'ky-huyen'}, - {label: 'Khoa Huyễn', value: 'khoa-huyen'}, - {label: 'Võng Du', value: 'vong-du'}, - {label: 'Đồng Nhân', value: 'dong-nhan'}, - - ] - } - } satisfies Filters; -} - -export default new TruyenConect(); diff --git a/plugins/vietnamese/Truyenconect.ts b/plugins/vietnamese/Truyenconect.ts new file mode 100644 index 0000000..fa0767d --- /dev/null +++ b/plugins/vietnamese/Truyenconect.ts @@ -0,0 +1,299 @@ + +import { CheerioAPI, load as parseHTML } from "cheerio"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { Plugin } from "@typings/plugin"; +import { FilterTypes, Filters } from "@libs/filterInputs"; +import { NovelStatus } from "@libs/novelStatus"; + +interface VolumeData { + story: string; + navigation: string; + value: string +} + +interface ChapterSelect { + chap_selector: string; + eps_selector: boolean | string; +} + +interface VolumeList { + chapters: Plugin.ChapterItem[], + volumes?: VolumeData[] +} + +class TruyenConect implements Plugin.PagePlugin { + id = "truyenconect"; + name = "Truyen Conect"; + icon = "src/vi/truyenconect/icon.png"; + site = "https://truyenconect.com"; + version = "1.0.0"; + async sleep (ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) + } + parseNovels(loadedCheerio: CheerioAPI, selector: string){ + const novels: Plugin.NovelItem[] = []; + loadedCheerio(selector).each((idx, ele) => { + const url = loadedCheerio(ele).find('a').attr('href'); + if(url){ + novels.push({ + name: loadedCheerio(ele).find('img').attr('alt') || '', + path: url.replace(this.site, ''), + cover: loadedCheerio(ele).find('img').attr('data-src'), + }) + } + }); + return novels; + } + async popularNovels(pageNo: number, {filters, showLatestNovels}: Plugin.PopularNovelsOptions): Promise { + let link = this.site; + let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover'; + + if(showLatestNovels){ + selector = '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover'; + }else if(filters.category.value){ + link += '/' + filters.category.value; + selector = 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]'; + if(filters.category.value === 'the-loai'){ + selector = '.item-thumb.hover-details.c-image-hover' + link += '/' + filters.genre.value; + } + link += '?page=' + pageNo; + } + + const body = await fetch(link).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio, selector); + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('option').each((idx, ele) => { + let url = ele.attribs['value']; + if(!url) return; + const chapterId = url.match(/\/(\d+)-/)?.[1]; + if(chapterId){ + url = url.replace(chapterId+'-', '') + '-' + chapterId; + } + let num = url.match(/chuong-(\d+)/)?.[1]; + chapters.push({ + path: url.replace(this.site, ''), + name: loadedCheerio(ele).text().trim(), + chapterNumber: Number(num) || undefined, + }) + }) + return chapters.reverse(); + } + parseVolumes(loadedCheerio: CheerioAPI) { + const volumes: VolumeData[] = []; + loadedCheerio('option').each((idx, ele) => { + volumes.push({ + story: ele.attribs['data-story'], + navigation: ele.attribs['data-navigation'], + value: ele.attribs['value'], + }) + }) + return volumes; + } + async getVolumes (firstChapterUrl: string) { + const chapterId = firstChapterUrl.match(/-(\d+)$/)?.[1]; + if(!chapterId) throw new Error('Không tìm thấy chương'); + const url = this.site + '/truyen/get-chap-selector?chap=' + chapterId; + const data: ChapterSelect = await fetch(url).then(r => r.json()); + const volumeList: VolumeList = { + chapters: this.parseChapters(parseHTML(data.chap_selector)) + } + if(data.eps_selector){ + volumeList.volumes = this.parseVolumes(parseHTML(data.eps_selector as string)) + } + return volumeList + } + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number; }= { + name: loadedCheerio(".post-title > h1").text().trim(), + path: novelPath, + chapters: [], + totalPages: 1 + }; + + novel.cover = loadedCheerio(".summary_image > a > img").attr( + "data-src" + ); + + loadedCheerio(".post-content_item").each(function () { + const detailName = loadedCheerio(this) + .find(".summary-heading > h5") + .text() + .trim(); + const detail = loadedCheerio(this).find(".summary-content").html(); + + if (!detail) return; + + switch (detailName) { + case "Thể loại": + novel.genres = loadedCheerio(detail) + .children("a") + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(","); + break; + case "Tác giả": + novel.author = loadedCheerio(detail).text().trim(); + break; + case "Hoạ sĩ": + novel.artist = loadedCheerio(detail).text().trim(); + break; + case "Trạng thái": + const status = detail.trim(); + if(status === 'Full') novel.status = NovelStatus.Completed; + else if(status === 'Tạm ngưng') novel.status = NovelStatus.OnHiatus; + else if(status === 'Đang tiến hành') novel.status = NovelStatus.Ongoing + else novel.status = NovelStatus.Unknown; + break; + } + }); + + loadedCheerio( + ".description-summary > div.summary__content > div" + ).remove(); + + novel.summary = loadedCheerio( + ".description-summary > div.summary__content" + ) + .text() + .trim(); + const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); + if(!firstChapLink) throw new Error('Không tìm thấy truyện'); + await this.sleep(1000); + const volumeList = await this.getVolumes(firstChapLink); + if(volumeList.volumes) { + novel.totalPages = volumeList.volumes.length; + novel.chapters = volumeList.chapters + }else{ + novel.totalPages = 1; + novel.chapters = volumeList.chapters; + } + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath; + const result = await fetch(url); + const body = await result.text(); + let loadedCheerio = parseHTML(body); + const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); + if(!firstChapLink) throw new Error('Không tìm thấy truyện'); + await this.sleep(1000); + const volumeList = await this.getVolumes(firstChapLink); + const volumeIndex = Number(page) - 1; + if(!volumeList.volumes) throw new Error("Không tìm thấy truyện"); + if(volumeIndex >= volumeList.volumes.length) throw new Error("Không tìm thấy volume"); + const volume = volumeList.volumes[volumeIndex]; + const query = `dataEpisodes=${volume.value}&datastory=${volume.story}&dataNavigation=${encodeURIComponent(volume.navigation)}`; + const chaptersUrl = `${this.site}/truyen/getreadingchapAction?${query}`; + await this.sleep(1000); + const res: {err: number, html: string} = await fetch(chaptersUrl).then(r => r.json()); + if(res.err) throw new Error(res.html); + return { + chapters: this.parseChapters(parseHTML(res.html)) + } + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + let chapterText = loadedCheerio(".reading-content").html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string, pageNo: number): Promise { + const url = `${this.site}?key=${searchTerm}&page=${pageNo}`; + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(".tab-thumb.c-image-hover > a").each((idx, ele) => { + const novelName = ele.attribs['title']; + const novelCover = loadedCheerio(ele).find('img').first().attr('src'); + const novelUrl = ele.attribs['href']; + + if (!novelUrl) return; + + novels.push({ + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }); + }); + + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } + filters = { + category: { + label: "Lọc theo", + value: '', + type: FilterTypes.Picker, + options: [ + {label: '', value: ''}, + {label: 'Thể loại', value: 'the-loai'}, + {label: 'Truyện dịch', value: 'truyen-dich'}, + {label: 'Truyện convert', value: 'convert'} + ] + }, + genre: { + label: 'Thể loại', + value: 'action', + type: FilterTypes.Picker, + options: [ + {label: 'Action', value: 'action'}, + {label: 'Adult', value: 'adult'}, + {label: 'Adventure', value: 'adventure'}, + {label: 'Chinese novel', value: 'chinese-novel'}, + {label: 'Chuyển Sinh', value: 'chuyen-sinh'}, + {label: 'English Novel', value: 'english-novel'}, + {label: 'Harem', value: 'harem'}, + {label: 'Ecchi', value: 'ecchi'}, + {label: 'Fantasy', value: 'fantasy'}, + {label: 'Drama', value: 'drama'}, + {label: 'Game', value: 'game'}, + {label: 'Tiên hiệp', value: 'tien-hiep'}, + {label: 'Kiếm Hiệp', value: 'kiem-hiep'}, + {label: 'Ngôn Tình', value: 'ngon-tinh'}, + {label: 'Isekai', value: 'isekai'}, + {label: 'Lịch Sử', value: 'lich-su'}, + {label: 'Web Novel', value: 'web-novel'}, + {label: 'Xuyên không', value: 'xuyen-khong'}, + {label: 'Trọng sinh', value: 'trong-sinh'}, + {label: 'Trinh thám', value: 'trinh-tham'}, + {label: 'Dị giới', value: 'di-gioi'}, + {label: 'Huyền ảo', value: 'huyen-ao'}, + {label: 'Sắc Hiệp', value: 'sac-hiep'}, + {label: 'Dị năng', value: 'di-nang'}, + {label: 'Linh dị', value: 'linh-di'}, + {label: 'Đô thị', value: 'do-thi'}, + {label: 'Comedy', value: 'comedy'}, + {label: 'School Life', value: 'school-life'}, + {label: 'Romance', value: 'romance'}, + {label: 'Martial-arts', value: 'martial-arts'}, + {label: 'Light Novel', value: 'light-novel'}, + {label: 'Huyền huyễn', value: 'huyen-huyen'}, + {label: 'Kỳ Huyễn', value: 'ky-huyen'}, + {label: 'Khoa Huyễn', value: 'khoa-huyen'}, + {label: 'Võng Du', value: 'vong-du'}, + {label: 'Đồng Nhân', value: 'dong-nhan'}, + ] + } + } satisfies Filters; +} + +export default new TruyenConect(); diff --git a/plugins/vietnamese/lightnovelvn.broken.ts b/plugins/vietnamese/lightnovelvn.broken.ts deleted file mode 100644 index b1e92e3..0000000 --- a/plugins/vietnamese/lightnovelvn.broken.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; - - -class LightNovelVN implements Plugin.PluginBase { - id = "lightnovel.vn"; - name = "Light Novel VN"; - version = "1.0.0"; - icon = "src/vi/lightnovelvn/icon.png"; - filters?: Filters | undefined; - site = "https://lightnovel.vn/"; - baseUrl = this.site; - headers = { - Accept: "application/json", - "Content-Type": "application/json", - }; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.baseUrl}truyen-hot-ds?page=${pageNo}`; - const body = await fetch(url).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => { - const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim(); - const img = loadedCheerio(ele).find('noscript').html(); - const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1]; - const novelUrl = this.site + loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href')?.substring(1); - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const body = await fetch(url).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio('h1[itemprop="name"]').text().trim(); - - novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0] - - const genres: string[] = []; - loadedCheerio('a[itemprop="genre"]').each(function() { - genres.push(loadedCheerio(this).text()); - }) - novel.genres = genres.join(','); - - novel.status = loadedCheerio('span.font-bold.text-size22:last').text(); - if(novel.status === 'Đang ra'){ - novel.status = NovelStatus.Ongoing; - }else if(novel.status === 'Hoàn thành'){ - novel.status = NovelStatus.Completed; - }else { - novel.status = NovelStatus.Unknown; - } - novel.author = loadedCheerio('a[itemprop="author"] > span').text(); - - novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' '); - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - await delay(1000); - const chapterListUrl = novelUrl + '/chapters?chorder=desc'; - - const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); - const loadedChapterList = parseHTML(chapterListBody); - - - const lastNumber = parseInt( - loadedChapterList('ul.chapter-list > li:nth-child(1)').attr('data-chapterno') ?? '0' - ); - const lastChapterUrl = loadedChapterList('ul.chapter-list > li:nth-child(1) > a').attr('href'); - - if(lastChapterUrl){ - const chapterUrlPrefix = this.baseUrl + lastChapterUrl.split('/').slice(1, -1).join('/'); - const chapters: Plugin.ChapterItem[] = []; - for(let i = 1; i <= lastNumber; i++){ - chapters.push({ - chapterNumber: i, - name: 'Chapter ' + i, - url: chapterUrlPrefix + '/chapter-' + i, - }) - } - novel.chapters = chapters; - } - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio("#chapter-container").html() || ''; - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}lnsearchlive`; - const link = `${this.baseUrl}search`; - const response = await fetchApi(link).then((r) => r.text()); - const token = parseHTML(response); - let verifytoken = token("#novelSearchForm > input").attr("value"); - - let formData = new FormData(); - formData.append("inputContent", searchTerm); - - const body = await fetchApi( - url, - { - method: "POST", - headers: { LNRequestVerifyToken: verifytoken! }, - body: formData, - }, - ).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - let results = JSON.parse(loadedCheerio("body").text()); - - loadedCheerio = parseHTML(results.resultview); - - loadedCheerio(".novel-item").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h4.novel-title") - .text() - .trim(); - const novelCover = loadedCheerio(ele).find("img").attr("src"); - const novelUrl = - this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.substring(1); - - novels.push({ - name: novelName, - url: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url, {headers: this.headers}); - } - -} - -export default new LightNovelVN(); \ No newline at end of file diff --git a/plugins/vietnamese/lightnovelvn.ts b/plugins/vietnamese/lightnovelvn.ts new file mode 100644 index 0000000..b05ecb7 --- /dev/null +++ b/plugins/vietnamese/lightnovelvn.ts @@ -0,0 +1,161 @@ +import { CheerioAPI, load as parseHTML } from "cheerio"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { Plugin } from "@typings/plugin"; +import { Filters } from "@libs/filterInputs"; +import { NovelStatus } from "@libs/novelStatus"; +import { NovelItem } from "../../test_web/static/js"; + +interface SearchedNovel { + name: string; + slug: string; + coverUrl: string; +} +interface SearchedResult { + data?: SearchedNovel[] +} + +class LightNovelVN implements Plugin.PagePlugin { + id = "lightnovel.vn"; + name = "Light Novel VN"; + version = "1.0.0"; + icon = "src/vi/lightnovelvn/icon.png"; + filters?: Filters | undefined; + site = "https://lightnovel.vn"; + async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { + const url = `${this.site}/truyen-hot-ds?page=${pageNo}`; + const body = await fetch(url).then((r) => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim(); + const img = loadedCheerio(ele).find('noscript').html(); + const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1]; + const novelUrl = loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href'); + if(novelUrl) + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }); + + return novels; + } + parseChapters(loadedCheerio: CheerioAPI){ + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul.chapter-list > li').each((idx, ele) => { + const chNum = Number(loadedCheerio(ele).find('div').first().text()); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + const name = loadedCheerio(ele).find('a').text().trim(); + if(chapterUrl){ + chapters.push({ + path: chapterUrl.replace(this.site, ''), + name, + chapterNumber: chNum + }) + } + }) + return chapters; + } + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const body = await fetch(url).then((r) => r.text()); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number; } = { + path: novelPath, + chapters: [], + name: 'Không có tiêu đề', + totalPages: 1 + }; + + novel.name = loadedCheerio('h1[itemprop="name"]').text().trim(); + + novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0] + + const genres: string[] = []; + loadedCheerio('a[itemprop="genre"]').each(function() { + genres.push(loadedCheerio(this).text()); + }) + novel.genres = genres.join(','); + + novel.status = loadedCheerio('span.font-bold.text-size22:last').text(); + if(novel.status === 'Đang ra'){ + novel.status = NovelStatus.Ongoing; + }else if(novel.status === 'Hoàn thành'){ + novel.status = NovelStatus.Completed; + }else { + novel.status = NovelStatus.Unknown; + } + novel.author = loadedCheerio('a[itemprop="author"] > span').text(); + + novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' '); + const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); + await delay(1000); + const chapterListUrl = url + '/danh-sach-chuong'; + + const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); + const loadedChapterList = parseHTML(chapterListBody); + novel.chapters = this.parseChapters(loadedChapterList); + loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => { + const href = ele.attribs['href']; + if(href){ + const page = Number(href.match(/\?page=(\d+)/)?.[1]); + if(page && page > novel.totalPages){ + novel.totalPages = page; + } + } + }) + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = `${this.site}${novelPath}/danh-sach-chuong?page=${page}`; + const chapterListBody = await fetchApi(url).then(r => r.text()); + const chapters = this.parseChapters(parseHTML(chapterListBody)); + return { + chapters + } + } + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); + + const loadedCheerio = parseHTML(body); + + let chapterText = loadedCheerio("div.chapter-content").html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string, pageNo: number): Promise { + if(pageNo > 1) return []; + const url = `${this.site}/api/book-search`; + let formData = new FormData(); + formData.append("keyword", searchTerm); + + const result: SearchedResult = await fetchApi( + url, + { + method: "POST", + body: formData, + }, + ).then((r) => r.json()); + + const novels: NovelItem[] = result.data?.map(item => { + return { + name: item.name, + path: '/truyen/' + item.slug, + cover: (this.site + item.coverUrl).replace('default.jpg', '150.jpg?w=256&q=') + } + }) || []; + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } + +} + +export default new LightNovelVN(); \ No newline at end of file diff --git a/plugins/vietnamese/nettruyen.ts b/plugins/vietnamese/nettruyen.ts new file mode 100644 index 0000000..66227f2 --- /dev/null +++ b/plugins/vietnamese/nettruyen.ts @@ -0,0 +1,108 @@ +import { fetchFile } from "@libs/fetch"; +import { Filters } from "@libs/filterInputs"; +import { NovelStatus } from "@libs/novelStatus"; +import { Plugin } from "@typings/plugin"; +import { CheerioAPI, load as parseHTML } from "cheerio"; + +class Nettruyen implements Plugin.PagePlugin { + id = 'nettruyen'; + name = 'Nettruyen'; + icon = 'src/vi/nettruyen/icon.png'; + site = 'https://nettruyen.com.vn'; + version = '1.0.0'; + parseNovels(loadedCheerio: CheerioAPI){ + const novels: Plugin.NovelItem[] = []; + loadedCheerio('#listchuong > ul > li > a.thumb').each((idx, ele) => { + const path = ele.attribs['href']; + if(!path) return; + const name = ele.attribs['title']; + const cover = this.site + '/' + loadedCheerio(ele).find('img').first().attr('src'); + novels.push({ + path: '/' + path, + name, + cover + }) + }) + return novels; + } + async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { + const url = `${this.site}/xem-nhieu/trang-${pageNo}.html`; + const body = await fetch(url).then(r => r.text()); + return this.parseNovels(parseHTML(body)); + } + parseChapters(loadedCheerio: CheerioAPI){ + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul.list-unstyled > li > a').each((idx, ele) => { + const path = ele.attribs['href']; + if(!path) return; + const name = ele.attribs['title']; + chapters.push({ + path: '/' + path, + name, + chapterNumber: Number(name.match(/Chương (\d+)/)?.[1]) + }) + }) + return chapters; + } + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const body = await fetch(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel & { totalPages: number; } = { + path: novelPath, + name: loadedCheerio('.gioithieutruyen.profile-info > h1').text().trim(), + chapters: [], + totalPages: Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1, + } + novel.cover = this.site + '/' + loadedCheerio('.img-thumb > img').attr('src'); + loadedCheerio('.thongtintruyen.note.note-info').each((idx, ele) => { + const strong = loadedCheerio(ele).find('strong').text().trim(); + switch(strong){ + case 'Tác giả': + novel.author = loadedCheerio(ele).text().replace(/Tác giả\s+\n?:/, '') + break; + case 'Trạng thái': + const text = loadedCheerio(ele).text(); + if(text.includes('Đang ra')){ + novel.status = NovelStatus.Ongoing; + }else if(text.includes('Hoàn thành')){ + novel.status = NovelStatus.Completed; + }else { + novel.status = NovelStatus.Unknown; + } + break; + case 'Thể loại': + novel.genres = loadedCheerio('a > span').toArray().map((gEle) => loadedCheerio(gEle).text()).join(','); + break; + default: + break; + } + }); + novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen').text().trim() + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const id = novelPath.match(/-(\d+)\//)?.[1]; + if(!id) throw new Error("Cant parse page"); + const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g,'')}&loai=truyendich`; + const body = await fetch(url).then(r => r.text()); + const chapters = this.parseChapters(parseHTML(body)); + return { + chapters + } + } + async parseChapter(chapterPath: string): Promise { + const url = this.site + chapterPath; + const body = await fetch(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return loadedCheerio('#noidungchap').html() || ''; + } + async searchNovels(searchTerm: string, pageNo: number): Promise { + throw new Error("Method not implemented."); + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } +} +export default new Nettruyen(); \ No newline at end of file diff --git a/plugins/vietnamese/truyenchu.ts b/plugins/vietnamese/truyenchu.ts new file mode 100644 index 0000000..e2bc9e3 --- /dev/null +++ b/plugins/vietnamese/truyenchu.ts @@ -0,0 +1,147 @@ +import { CheerioAPI, load as parseHTML } from "cheerio"; +import { fetchFile } from "@libs/fetch"; +import { Plugin } from "@typings/plugin"; +import { Filters } from "@libs/filterInputs"; +import { NovelStatus } from "@libs/novelStatus"; + +class TruyenFull implements Plugin.PagePlugin { + id = "truyenchu"; + name = "Truyện Chữ"; + icon = "src/vi/truyenchu/icon.png"; + site = "https://truyenchu.vn"; + version = "1.0.0"; + + parseNovels(loadedCheerio: CheerioAPI){ + const novels: Plugin.NovelItem[] = []; + loadedCheerio(".list-truyen .row").each((idx, ele) => { + const novelName = loadedCheerio(ele) + .find("h3.truyen-title > a") + .text(); + + const novelCover = this.site + loadedCheerio(ele).find("div[data-classname='cover']").attr("data-image"); + + const novelUrl = loadedCheerio(ele) + .find("h3.truyen-title > a") + .attr("href"); + if(novelUrl){ + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, '') + }); + } + }); + return novels; + } + parseChapters(html: string){ + const listChapterHTML = + html.match(/("list_chapter":\s?\\{0}"(.+)\\{0}"),/)?.[1]; + if(!listChapterHTML) throw new Error("Không tải được chương"); + const listChapter = JSON.parse(`{${listChapterHTML}}`); + const loadedChapterList = parseHTML(listChapter.list_chapter); + const chapters: Plugin.ChapterItem[] = []; + loadedChapterList('ul > li > a').each((idx, ele) => { + const path = ele.attribs['href'].replace(this.site, ''); + if(path){ + chapters.push({ + name: ele.attribs['title'], + path, + chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1]) + }) + } + }); + return chapters; + } + async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { + const url = `${this.site}/danh-sach/truyen-hot?page=${pageNo}`; + const result = await fetch(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + let lastPage = 1; + const regex = new RegExp(`${novelPath}\\?page=\\d+`, 'g'); + body.match(regex)?.forEach(v => { + const page = Number(v.match(/\?page=(\d+)/)?.[1]); + if(page && page > lastPage){ + lastPage = page; + } + }); + const novel: Plugin.SourceNovel & {totalPages: number} = { + path: novelPath, + name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề', + chapters: [], + totalPages: lastPage, + }; + + novel.cover = this.site + loadedCheerio("div.book > img").attr("src"); + + novel.summary = loadedCheerio("div.desc-text").text().trim(); + + novel.author = loadedCheerio('h3:contains("Tác giả:")') + .parent() + .contents() + .text() + .replace("Tác giả:", ""); + + novel.genres = loadedCheerio('h3:contains("Thể loại")') + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(","); + + novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); + if(novel.status === "Full"){ + novel.status = NovelStatus.Completed; + }else if(novel.status === 'Đang ra'){ + novel.status = NovelStatus.Ongoing; + }else{ + novel.status = NovelStatus.Unknown; + } + novel.chapters = this.parseChapters(body); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = `${this.site}${novelPath}?page=${page}`; + const result = await fetch(url); + const body = await result.text(); + + const chapters = this.parseChapters(body); + return { + chapters + } + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let chapterText = (loadedCheerio(".chapter-title").html() || '') + (loadedCheerio('#chapter-c').html() || ''); + + return chapterText; + } + async searchNovels(searchTerm: string, pageNo: number): Promise { + const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; + + const result = await fetch(searchUrl); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio) + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } + +} + +export default new TruyenFull(); diff --git a/plugins/vietnamese/truyenfull.ts b/plugins/vietnamese/truyenfull.ts index 677df8b..59638ed 100644 --- a/plugins/vietnamese/truyenfull.ts +++ b/plugins/vietnamese/truyenfull.ts @@ -4,7 +4,7 @@ import { Plugin } from "@typings/plugin"; import { Filters } from "@libs/filterInputs"; import { NovelStatus } from "@libs/novelStatus"; -class TruyenFull implements Plugin.PluginBase { +class TruyenFull implements Plugin.PagePlugin { id = "truyenfull"; name = "Truyện Full"; icon = "src/vi/truyenfull/icon.png"; @@ -27,13 +27,22 @@ class TruyenFull implements Plugin.PluginBase { novels.push({ name: novelName, cover: novelCover, - url: novelUrl + path: novelUrl.replace(this.site, '') }); } }); return novels; } - + parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[]{ + return loadedCheerio('ul.list-chapter > li > a').toArray().map((ele) => { + const path = ele.attribs['href'].replace(this.site, ''); + return { + name: loadedCheerio(ele).text().trim(), + path, + chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1]) + } + }) + } async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { const url = `${this.site}/danh-sach/truyen-hot/trang-${pageNo}/`; @@ -44,21 +53,26 @@ class TruyenFull implements Plugin.PluginBase { return this.parseNovels(loadedCheerio); } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; const result = await fetch(url); const body = await result.text(); let loadedCheerio = parseHTML(body); + let lastPage = 1; + loadedCheerio('ul.pagination.pagination-sm > li > a').each(function() { + const page = Number(this.attribs['href'].match(/\/trang-(\d+)\//)?.[1]); + if(page && page > lastPage) lastPage = page; + }); - const novel: Plugin.SourceNovel = { - url, + const novel: Plugin.SourceNovel & {totalPages: number} = { + path: novelPath, + name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề', chapters: [], + totalPages: lastPage, }; - novel.name = loadedCheerio("div.book > img").attr("alt"); - novel.cover = loadedCheerio("div.book > img").attr("src"); novel.summary = loadedCheerio("div.desc-text").text().trim(); @@ -83,30 +97,22 @@ class TruyenFull implements Plugin.PluginBase { }else{ novel.status = NovelStatus.Unknown; } - - let lastPage = 1; - loadedCheerio('ul.pagination.pagination-sm > li').each(function() { - const page = Number(loadedCheerio(this).text()); - if(page && page > lastPage) lastPage = page; - }); - - const lastPageUrl = novelUrl + 'trang-' + lastPage + '/'; - const res = await fetch(lastPageUrl).then(res => res.text()); - loadedCheerio = parseHTML(res); - const lastChapterUrl = loadedCheerio('ul.list-chapter > li:last-child > a').attr('href'); - const lastChapter = Number(lastChapterUrl?.match(/\/chuong-(\d+)\//)?.[1]) || 1; - const chapters: Plugin.ChapterItem[] = []; - for(let i = 1; i <= lastChapter; i++){ - chapters.push({ - name: 'Chương ' + i, - url: novelUrl + 'chuong-'+ i + '/', - }) - } - novel.chapters = chapters; + novel.chapters = this.parseChapters(loadedCheerio); return novel; } - async parseChapter(chapterUrl: string): Promise { - const result = await fetch(chapterUrl); + async parsePage(novelPath: string, page: string): Promise { + const url = `${this.site}${novelPath}trang-${page}/#list-chapter`; + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return { + chapters + } + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); diff --git a/scripts/json_plugins.ts b/scripts/json_plugins.ts index 3c8a6ad..c6df734 100644 --- a/scripts/json_plugins.ts +++ b/scripts/json_plugins.ts @@ -3,7 +3,8 @@ require("module-alias/register"); import * as fs from "fs"; import { languages } from "@libs/languages"; import * as path from "path"; -import { Plugin, isPlugin } from "@typings/plugin"; +import { Plugin } from "@typings/plugin"; +import { minify } from "./terser"; const root = path.dirname(__dirname); const outRoot = path.join(root, ".."); @@ -11,16 +12,16 @@ const { execSync } = require("child_process"); const REMOTE = execSync("git remote get-url origin") .toString() .replace(/[\s\n]/g, ""); -let CURRENT_BRANCH = 'dist'; +let CURRENT_BRANCH = "dist"; try { CURRENT_BRANCH = execSync("git rev-parse --abbrev-ref HEAD") .toString() .replace(/[\s\n]/g, ""); -}catch { +} catch { // } const matched = REMOTE.match(/([^:/]+?)\/([^/.]+)(\.git)?$/); -if(!matched) throw Error("Cant parse git url"); +if (!matched) throw Error("Cant parse git url"); const USERNAME = matched[1]; const REPO = matched[2]; const USER_CONTENT_LINK = `https://raw.githubusercontent.com/${USERNAME}/${REPO}/${CURRENT_BRANCH}`; @@ -54,15 +55,10 @@ for (let language in languages) { json[language] = []; plugins.forEach((plugin) => { if (plugin.startsWith(".")) return; - const instance: Plugin.PluginBase | unknown = - require(`../plugins/${language.toLowerCase()}/${ - plugin.split(".")[0] - }`).default; - - if (!isPlugin(instance)) { - console.log(plugin); - return; - } + minify(path.join(langPath, plugin)); + const instance: Plugin.PluginBase = require( + `../plugins/${language.toLowerCase()}/${plugin.split(".")[0]}`, + ).default; const { id, name, site, version, icon } = instance; const normalisedName = name.replace(/\[.*\]/, ""); @@ -83,7 +79,6 @@ for (let language in languages) { } else { pluginSet.add(id); } - json[language].push(info); console.log(name, "✅"); }); diff --git a/scripts/multisrc/generate.ts b/scripts/multisrc/generate.ts index c4baea7..44d062a 100644 --- a/scripts/multisrc/generate.ts +++ b/scripts/multisrc/generate.ts @@ -50,14 +50,13 @@ const run = async () => { const sources = fs .readdirSync(__dirname) .filter((name) => - fs.lstatSync(path.join(__dirname, name)).isDirectory() + fs.lstatSync(path.join(__dirname, name)).isDirectory() && !name.endsWith('.broken') ); for (let name of sources) { const success = await generate(name); if (success) console.log(`[${name}] OK`); } - console.log('Generating: .js files'); }; run(); diff --git a/scripts/multisrc/hotnovelpub/filter_refresh.ts b/scripts/multisrc/hotnovelpub/filter_refresh.ts new file mode 100644 index 0000000..788b107 --- /dev/null +++ b/scripts/multisrc/hotnovelpub/filter_refresh.ts @@ -0,0 +1,83 @@ +import * as fs from "fs"; +import * as cheerio from "cheerio"; +import * as path from "path"; +import list from "./sources.json"; +import { HotNovelPubMetadata } from "./template"; + +async function getFilters(sources: HotNovelPubMetadata) { + const filters: any = { + sort: { + type: "Picker", + label: "Order", + value: "hot", + options: [], + }, + category: { + type: "Picker", + label: "category", + value: "", + options: [{ label: "NONE", value: "" }], + }, + }; + const body = await fetch(sources.sourceSite).then((res) => res.text()); + const $: cheerio.CheerioAPI = cheerio.load(body); + $(".new-update").remove(); + + $("section > div").each((i, el) => { + const id = $(el).find('a[class="see-all"]').attr("href"); + if (id) { + filters.sort.options.push({ + label: $(el).find('[class="section-title"]').text().trim(), + value: id.split("/").pop(), + }); + } + }); + + filters.category.label = $(".category-title").text().trim(); + const apiSite = sources.sourceSite.replace("://", "://api."); + const jsonRaw = await fetch(apiSite + "/categories", { + headers: { + lang: sources.options?.lang || "en", + }, + }); + const json = (await jsonRaw.json()) as response; + + if (json.data?.length) { + json.data + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach((category) => + filters.category.options.push({ + label: category.name, + value: category.slug, + }), + ); + } + return filters; +} + +interface response { + status: number; + message: string; + data?: DataEntity[]; +} +interface DataEntity { + id: number; + name: string; + slug: string; +} + +async function start() { + const result = []; + for (const sources of list) { + console.log("updating the filters in", sources.sourceName); + const NewFilters = await getFilters(sources as any); + sources.filters = NewFilters; + result.push(sources); + } + fs.writeFileSync( + path.join(__dirname, "sources.json"), + JSON.stringify(result, null, 2), + ); +} + +start(); diff --git a/scripts/multisrc/hotnovelpub/generator.ts b/scripts/multisrc/hotnovelpub/generator.ts new file mode 100644 index 0000000..8632210 --- /dev/null +++ b/scripts/multisrc/hotnovelpub/generator.ts @@ -0,0 +1,58 @@ +import { FilterTypes, Filters } from "../../../libs/filterInputs"; +import { ScrpitGeneratorFunction } from "../generate"; +import list from "./sources.json"; +import { HotNovelPubMetadata } from "./template"; +import { readFileSync } from "fs"; +import path from "path"; + +const lang: { [key: string]: string } = { + en: "english", + ru: "russian", + es: "spanish", + pt: "portuguese", + th: "turkish", +}; + +export const generateAll: ScrpitGeneratorFunction = function (name) { + return list + .map((p) => { + const filters: Filters = {}; + for (const k in p.filters) { + const f = p.filters[k as keyof typeof p.filters]; + if (f) { + filters[k] = { + ...f, + type: FilterTypes.Picker, + }; + } + } + return { ...p, filters }; + }) + .map((metadata: HotNovelPubMetadata) => { + console.log(`[${name}]: Generating`, metadata.id); + return generator(metadata); + }); +}; + +const generator = function generator(metadata: HotNovelPubMetadata) { + const HotNovelPubTemplate = readFileSync( + path.join(__dirname, "template.ts"), { + encoding: "utf-8", + }, + ); + + const pluginScript = ` + ${HotNovelPubTemplate} +const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata).replace( + /"type":"([^"]+)"/g, + '"type":FilterTypes.$1', + )}); +export default plugin; + `.trim(); + + return { + lang: lang[metadata?.options?.lang || "en"] || "english", + filename: metadata.sourceName, + pluginScript, + }; +}; diff --git a/scripts/multisrc/hotnovelpub/sources.json b/scripts/multisrc/hotnovelpub/sources.json new file mode 100644 index 0000000..5d56c9a --- /dev/null +++ b/scripts/multisrc/hotnovelpub/sources.json @@ -0,0 +1,387 @@ +[ + { + "id": "hotnovelpub", + "sourceSite": "https://hotnovelpub.com", + "sourceName": "HotNovelPub", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "Hot story", + "value": "hot" + }, + { + "label": "New story", + "value": "new" + }, + { + "label": "Full story", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Category", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Adult Romance", + "value": "adult-romance" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "LGBTQ+", + "value": "lgbtq" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "New Adult", + "value": "new-adult" + }, + { + "label": "Paranormal", + "value": "paranormal" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Steamy Stories", + "value": "steamy-stories" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Werewolf/Vampire", + "value": "werewolfvampire" + } + ] + } + } + }, + { + "id": "eznovels", + "sourceSite": "https://eznovels.com", + "sourceName": "EzNovels", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "Горячая история", + "value": "hot" + }, + { + "label": "Новая история", + "value": "new" + }, + { + "label": "Полный рассказ", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Категория", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Ведьмы", + "value": "vedmy" + }, + { + "label": "Городское фэнтези", + "value": "gorodskoe-fentezi" + }, + { + "label": "Городской роман", + "value": "gorodskoj-roman" + }, + { + "label": "Любовное фэнтези", + "value": "lyubovnoe-fentezi" + }, + { + "label": "Оборотни", + "value": "oborotni" + }, + { + "label": "Сверхъестественное", + "value": "sverhuestestvennoe" + }, + { + "label": "Современный любовный роман", + "value": "sovremennyj-lyubovnyj-roman" + }, + { + "label": "Триллеры", + "value": "trillery" + }, + { + "label": "Фэнтези", + "value": "fentezi" + }, + { + "label": "Эротика", + "value": "erotika" + } + ] + } + }, + "options": { + "lang": "ru" + } + }, + { + "id": "lightnoveldaily", + "sourceSite": "https://lightnoveldaily.com", + "sourceName": "LightNovelDaily", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "Historia caliente", + "value": "hot" + }, + { + "label": "Nuevas cuentos", + "value": "new" + }, + { + "label": "Historia completa", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Categoría", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Acción/Aventura", + "value": "accinaventura" + }, + { + "label": "Fantasía", + "value": "fantasa" + }, + { + "label": "Ficción adolescente", + "value": "ficcin-adolescente" + }, + { + "label": "Hombre lobo/Vampiro", + "value": "hombre-lobovampiro" + }, + { + "label": "LGBTQ+", + "value": "lgbtq" + }, + { + "label": "Mafia", + "value": "mafia" + }, + { + "label": "Nuevo Adulto", + "value": "nuevo-adulto" + }, + { + "label": "Pornografía", + "value": "pornografa" + }, + { + "label": "Romántico", + "value": "romntico" + }, + { + "label": "Urbano", + "value": "urbano" + } + ] + } + }, + "options": { + "lang": "es" + } + }, + { + "id": "lanovels", + "sourceSite": "https://lanovels.com", + "sourceName": "LaNovels", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "História quente", + "value": "hot" + }, + { + "label": "Nova estória", + "value": "new" + }, + { + "label": "História completa", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Categoria", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Erótico", + "value": "erotico" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Ficção adolescente", + "value": "ficcao-adolescente" + }, + { + "label": "Ficção científica", + "value": "ficcao-cientifica" + }, + { + "label": "Lobisomens/Vampiros", + "value": "lobisomens-vampiros" + }, + { + "label": "Mistério", + "value": "misterio" + }, + { + "label": "Paranormal", + "value": "paranormal" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Suspense/Terror", + "value": "suspense-terror" + } + ] + } + }, + "options": { + "lang": "pt" + } + }, + { + "id": "thnovels", + "sourceSite": "https://thnovels.com", + "sourceName": "ThNovels", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "เรื่องร้อน", + "value": "hot" + }, + { + "label": "เรื่องใหม่", + "value": "new" + }, + { + "label": "เรื่องเต็ม", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "หมวดหมู่", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "ใช้ชีวิต", + "value": "ใช้ชีวิต" + }, + { + "label": "นิยายY", + "value": "นิยายy" + }, + { + "label": "นิยายสำหรับผู้ใหญ่", + "value": "นิยายสำหรับผู้ใหญ่" + }, + { + "label": "ประวัติศาสตร์", + "value": "ประวัติศาสตร์" + }, + { + "label": "แฟนตาซี", + "value": "แฟนตาซี" + }, + { + "label": "มนุษย์หมาป่าแวมไพร์", + "value": "มนุษย์หมาป่าแวมไพร์" + }, + { + "label": "โรแมนซ์", + "value": "โรแมนซ์" + } + ] + } + }, + "options": { + "lang": "th" + } + } +] \ No newline at end of file diff --git a/scripts/multisrc/hotnovelpub/template.ts b/scripts/multisrc/hotnovelpub/template.ts new file mode 100644 index 0000000..87fb93d --- /dev/null +++ b/scripts/multisrc/hotnovelpub/template.ts @@ -0,0 +1,279 @@ +import { fetchFile, fetchApi } from "@libs/fetch"; +import { Filters, FilterTypes } from "@libs/filterInputs"; +import { Plugin } from "@typings/plugin"; +import { NovelStatus } from "@libs/novelStatus"; +import { load as parseHTML } from "cheerio"; +import dayjs from "dayjs"; + +export interface HotNovelPubMetadata { + id: string; + sourceSite: string; + sourceName: string; + filters?: Filters; + options?: HotNovelPubOptions; +} + +interface HotNovelPubOptions { + lang?: string; +} + +class HotNovelPubPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + apiSite: string; + version: string; + filters?: Filters; + lang: string; + + constructor(metadata: HotNovelPubMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/hotnovelpub/icons/${metadata.id}.png`; + this.site = metadata.sourceSite; + this.apiSite = metadata.sourceSite.replace("://", "://api."); + this.version = "1.0.0"; + this.filters = metadata.filters; + this.lang = metadata.options?.lang || "en"; + } + + async popularNovels( + pageNo: number, + { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.apiSite + "/books/"; + url += showLatestNovels ? "new" : filters?.sort?.value || "hot"; + if (filters?.category?.value) { + url = this.apiSite + "/category/" + filters.category.value; + } + + url += "/?page=" + (pageNo - 1) + "&limit=20"; + + const result = await fetchApi(url, { + headers: { + lang: this.lang, + }, + }); + const json = (await result.json()) as responseNovels; + const novels: Plugin.NovelItem[] = []; + + if (json.status && json.data.books.data?.length) { + json.data.books.data.forEach((novel) => + novels.push({ + name: novel.name, + cover: this.site + novel.image, + path: "/" + novel.slug, + }), + ); + } + return novels; + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.apiSite + "/book" + novelPath, { + headers: { + lang: this.lang, + }, + }); + const json = (await result.json()) as responseNovel; + + const novel: Plugin.SourceNovel = { + name: json.data.book.name, + path: novelPath, + cover: this.site + json.data.book.image, + summary: json.data.book.authorize.description, + author: json.data.book.authorize.name, + status: + json.data.book.status === "updating" + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + if (json.data.tags.tags_name?.length) { + novel.genres = json.data.tags.tags_name.join(","); + } + + if (json.data.chapters?.length) { + const chapters: Plugin.ChapterItem[] = []; + json.data.chapters.forEach((chapter, chapterIndex) => + chapters.push({ + name: chapter.title, + path: "/" + chapter.slug, + releaseTime: undefined, + chapterNumber: (chapter.index || chapterIndex) + 1, + }), + ); + + novel.chapters = chapters; + } + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const loadedCheerio = parseHTML(body); + + let chapterText = loadedCheerio("#content-item").html() || ""; + if (chapterText) { + const result = await fetchApi( + this.site + "/server/getContent?slug=" + chapterPath, + ); + const json = (await result.json()) as ChapterType; + + if (json.data) { + chapterText += json.data + .map((item) => "

" + item + "

") + .join("") + .replace(/\n/g, "

") + .replace(/\s/g, " "); + } + } + return chapterText.replace(/\.copy right hot novel pub/g, ""); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const result = await fetchApi(this.apiSite + "/search", { + headers: { + "Content-Type": "application/json;charset=utf-8", + Referer: this.site, + Origin: this.site, + lang: this.lang, + }, + method: "POST", + body: JSON.stringify({ key_search: searchTerm }), + }); + const json = (await result.json()) as responseSearch; + const novels: Plugin.NovelItem[] = []; + + if (json.status && json.data.books?.length) { + json.data.books.forEach((novel) => + novels.push({ + name: novel.name, + cover: undefined, + path: "/" + novel.slug, + }), + ); + } + + return novels; + } + + fetchImage = fetchFile; +} + +interface responseNovels { + status: number; + message: string; + data: Data; +} +interface Data { + category?: Category; + books: Books; +} +interface Category { + id: number; + name: string; + description: string; + slug: string; + createdAt: string; + updatedAt: string; + countryId: number; +} +interface Books { + total: number; + pages_count: number; + data?: DataEntity[]; +} +interface DataEntity { + id: number; + name: string; + view: number; + status: string; + image: string; + slug: string; + categories?: CategoriesEntity[] | null; + source?: null; +} +interface CategoriesEntity { + id: number; + name: string; + slug?: string; +} + +interface responseNovel { + status: number; + message: string; + data: Data1; +} +interface Data1 { + book: Book1; + tags: Tags; + chapters?: ChaptersEntity[]; +} +interface Book1 { + id: number; + name: string; + description: string; + view: number; + rate: number; + status: string; + image: string; + createdAt: string; + chapterCount: number; + crawlStt: string; + rateCount: number; + slug: string; + categories?: CategoriesEntity[] | null; + authorize: Authorize; + updated_at: string; + source?: null; + idSource?: null; + prize?: null; +} +interface Authorize { + id: number; + name: string; + description: string; + avatar: string; + slug: string; +} +interface Tags { + tags_name?: string[] | null; + tags?: CategoriesEntity[] | null; +} +interface ChaptersEntity { + id: number; + title: string; + slug: string; + index: number; +} + +interface ChapterType { + type: string; + data?: string[]; +} + +interface responseSearch { + status: number; + message: string; + data: Data2; +} +interface Data2 { + books?: BooksEntity[] | null; + authorizes?: AuthorizesEntityOrCategoriesEntity[] | null; + categories?: AuthorizesEntityOrCategoriesEntity[] | null; +} +interface BooksEntity { + id: number; + name: string; + slug: string; +} +interface AuthorizesEntityOrCategoriesEntity { + id: number; + name: string; + slug: string; +} diff --git a/scripts/multisrc/ifreedom/template.ts b/scripts/multisrc/ifreedom/template.ts index a3bf5ae..13d0ef1 100644 --- a/scripts/multisrc/ifreedom/template.ts +++ b/scripts/multisrc/ifreedom/template.ts @@ -31,7 +31,10 @@ class IfreedomPlugin implements Plugin.PluginBase { async popularNovels( page: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + "/vse-knigi/?sort=" + (showLatestNovels ? "По дате обновления" : filters?.sort?.value || "По рейтингу"); @@ -53,20 +56,21 @@ class IfreedomPlugin implements Plugin.PluginBase { .map((index, element) => ({ name: loadedCheerio(element).attr("title") || "", cover: loadedCheerio(element).find("img").attr("src"), - url: loadedCheerio(element).attr("href") || "", + path: + loadedCheerio(element).attr("href")?.replace?.(this.site, "") || "", })) .get() - .filter((novel) => novel.name && novel.url); + .filter((novel) => novel.name && novel.path); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const body = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, name: loadedCheerio(".entry-title").text(), cover: loadedCheerio(".img-ranobe > img").attr("src"), summary: loadedCheerio('meta[name="description"]').attr("content"), @@ -105,12 +109,16 @@ class IfreedomPlugin implements Plugin.PluginBase { const name = loadedCheerio(element).find("a").text(); const url = loadedCheerio(element).find("a").attr("href"); if (!loadedCheerio(element).find("label.buy-ranobe").length && name && url) { - const releaseDate = loadedCheerio(element).find("div.li-col2-ranobe").text().trim(); - chapters.push({ + const releaseDate = loadedCheerio(element) + .find("div.li-col2-ranobe") + .text() + .trim(); + + chapters.push({ name, - url, + path: url.replace(this.site, ""), releaseTime: this.parseDate(releaseDate), - chapterNumber: totalChapters - chapterIndex + chapterNumber: totalChapters - chapterIndex, }); } }); @@ -119,8 +127,8 @@ class IfreedomPlugin implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); loadedCheerio(".entry-content img").each((index, element) => { @@ -153,10 +161,10 @@ class IfreedomPlugin implements Plugin.PluginBase { .map((index, element) => ({ name: loadedCheerio(element).attr("title") || "", cover: loadedCheerio(element).find("img").attr("src"), - url: loadedCheerio(element).attr("href") || "", + path: loadedCheerio(element).attr("href")?.replace?.(this.site, "") || "", })) .get() - .filter((novel) => novel.name && novel.url); + .filter((novel) => novel.name && novel.path); return novels; } diff --git a/scripts/multisrc/lightnovelwp/README.md b/scripts/multisrc/lightnovelwp/README.md new file mode 100644 index 0000000..268c92a --- /dev/null +++ b/scripts/multisrc/lightnovelwp/README.md @@ -0,0 +1,32 @@ +# Compatiblity +### This generator is for most sites that uses the LightNovel WordPress Theme: https://themesia.com/lightnovel-wordpress-theme/ + +it should work for all sites that uses this theme (highest version tested: 1.1.5) + +to know the name and version of the theme you can enter the url in : [WP Theme Detector](https://www.wpthemedetector.com) +(it sometimes hallucinates for version 1.0.X as 1.0) \ +or you can check the version by adding "/wp-content/themes/lightnovel/style.css" +to the site url and check the version in the file + + + +## ADD A NEW SOURCE +To add a new source you need to add it to sources.json: +- id: the id of the source (something unique) +- sourceName: the name of the source (the name that will be displayed in the app) +- sourceSite: the site url +- options: the options of the source + - lang: the language of the source (default: "English") (check that the language + exists in the languages (check folder names in "plugins/")) + - reverseChapter: if the chapters are in reverse order + (if the first chapter of the list is 1 set it to false) + +To add an icon to the source you need to find the icon of the site \ +(try the favicon of the site (https://site.com/favicon.ico) most of the times it redirects you to an image named something like "cropped-site-32x32.png" try to access "cropped-site.png" or "site.png" if that did not work you can try to access "https://site.com/wp-json/" at the end of this very long file there should be a "site_icon_url" value +) (don't forget to convert it to png) +and add it to the folder "icons/multisrc/lightnovelwp" named as the id of the source + +To add filters to a source you need to run the script "get_filters.ts" \ +(./node_modules/.bin/ts-node scripts/multisrc/lightnovelwp/get_filters.ts +(if you are in the root of the project) (and you have run "npm install" before)) +and follow the instructions (url is easier and faster but sometimes it doesn't work) diff --git a/scripts/multisrc/lightnovelwp/filters/allnovelread.json b/scripts/multisrc/lightnovelwp/filters/allnovelread.json new file mode 100644 index 0000000..7c58039 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/allnovelread.json @@ -0,0 +1,1076 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "16+", + "value": "16" + }, + { + "label": "Abogado/Abogada", + "value": "abogado-abogada" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Advogdo", + "value": "advogdo" + }, + { + "label": "affair of the heart", + "value": "affair-of-the-heart" + }, + { + "label": "alfa", + "value": "alfa" + }, + { + "label": "Alpha", + "value": "alpha" + }, + { + "label": "Amable", + "value": "amable" + }, + { + "label": "Amar", + "value": "amar" + }, + { + "label": "Amor", + "value": "amor" + }, + { + "label": "Amor caliente", + "value": "amor-caliente" + }, + { + "label": "amor depois do casamento", + "value": "amor-depois-do-casamento" + }, + { + "label": "Amor después del matrimonio", + "value": "amor-despues-del-matrimonio" + }, + { + "label": "Amor destinado", + "value": "amor-destinado" + }, + { + "label": "Amor doloroso", + "value": "amor-doloroso" + }, + { + "label": "Amor dulce", + "value": "amor-dulce" + }, + { + "label": "Amor e ódio", + "value": "amor-e-odio" + }, + { + "label": "Amor e ódio Gravidez", + "value": "amor-e-odio-gravidez" + }, + { + "label": "amor entre ex", + "value": "amor-entre-ex" + }, + { + "label": "Amor forzado", + "value": "amor-forzado" + }, + { + "label": "Amor Inocente", + "value": "amor-inocente" + }, + { + "label": "amor predestinado", + "value": "amor-predestinado" + }, + { + "label": "Amor y odio", + "value": "amor-y-odio" + }, + { + "label": "arrepentirse del divorcio", + "value": "arrepentirse-del-divorcio" + }, + { + "label": "Arrepentirse Después de Herir a Su Mujer", + "value": "arrepentirse-despues-de-herir-a-su-mujer" + }, + { + "label": "Arrogante", + "value": "arrogante" + }, + { + "label": "Asesinato", + "value": "asesinato" + }, + { + "label": "Babby", + "value": "babby" + }, + { + "label": "BABY", + "value": "baby" + }, + { + "label": "Beauty", + "value": "beauty" + }, + { + "label": "Bebê fofo", + "value": "bebe-fofo" + }, + { + "label": "Bebé inteligente", + "value": "bebe-inteligente" + }, + { + "label": "belleza", + "value": "belleza" + }, + { + "label": "Belleza inusual", + "value": "belleza-inusual" + }, + { + "label": "Bilionário", + "value": "bilionario" + }, + { + "label": "Billionair", + "value": "billionair" + }, + { + "label": "billionaire", + "value": "billionaire" + }, + { + "label": "Billonario/Billonaria", + "value": "billonario-billonaria" + }, + { + "label": "brilliant", + "value": "brilliant" + }, + { + "label": "bxg", + "value": "bxg" + }, + { + "label": "Bxg-novela", + "value": "bxg-novela" + }, + { + "label": "Campus", + "value": "campus" + }, + { + "label": "Casamiento", + "value": "casamiento" + }, + { + "label": "CEO", + "value": "ceo" + }, + { + "label": "city", + "value": "city" + }, + { + "label": "Colegiala", + "value": "colegiala" + }, + { + "label": "Comedia", + "value": "comedia" + }, + { + "label": "Comedia-novela", + "value": "comedia-novela" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "contemporáneo", + "value": "contemporaneo" + }, + { + "label": "Contract marriage", + "value": "contract-marriage" + }, + { + "label": "cónyuge", + "value": "conyuge" + }, + { + "label": "Corazón roto", + "value": "corazon-roto" + }, + { + "label": "courtship", + "value": "courtship" + }, + { + "label": "Crecimiento del personaje", + "value": "crecimiento-del-personaje" + }, + { + "label": "Crimen organizado", + "value": "crimen-organizado" + }, + { + "label": "Critical", + "value": "critical" + }, + { + "label": "cruel", + "value": "cruel" + }, + { + "label": "De pobre a rico", + "value": "de-pobre-a-rico" + }, + { + "label": "Divertido", + "value": "divertido" + }, + { + "label": "Divorce", + "value": "divorce" + }, + { + "label": "Divorcio", + "value": "divorcio" + }, + { + "label": "Doce", + "value": "doce" + }, + { + "label": "Doctor", + "value": "doctor" + }, + { + "label": "Dominador", + "value": "dominador" + }, + { + "label": "Dominante", + "value": "dominante" + }, + { + "label": "Dominante-novela", + "value": "dominante-novela" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "dulce", + "value": "dulce" + }, + { + "label": "Dulce Embarazada", + "value": "dulce-embarazada" + }, + { + "label": "elegante", + "value": "elegante" + }, + { + "label": "Embarazada", + "value": "embarazada" + }, + { + "label": "En la actualidad", + "value": "en-la-actualidad" + }, + { + "label": "Enemigos a los amantes", + "value": "enemigos-a-los-amantes" + }, + { + "label": "existente", + "value": "existente" + }, + { + "label": "Family", + "value": "family" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fated", + "value": "fated" + }, + { + "label": "Fraco para forte/Pob", + "value": "fraco-para-forte-pob" + }, + { + "label": "fuerte", + "value": "fuerte" + }, + { + "label": "Goodgirl", + "value": "goodgirl" + }, + { + "label": "Gravidez", + "value": "gravidez" + }, + { + "label": "HE", + "value": "he" + }, + { + "label": "heir/heiress", + "value": "heir-heiress" + }, + { + "label": "hermoso", + "value": "hermoso" + }, + { + "label": "Héroe pateador", + "value": "heroe-pateador" + }, + { + "label": "Heroina", + "value": "heroina" + }, + { + "label": "heroína Kickass", + "value": "heroina-kickass" + }, + { + "label": "heterose*ual", + "value": "heteroseual" + }, + { + "label": "historia de amor", + "value": "historia-de-amor" + }, + { + "label": "Hot Romance", + "value": "hot-romance" + }, + { + "label": "Humor", + "value": "humor" + }, + { + "label": "Identidad secreta", + "value": "identidad-secreta" + }, + { + "label": "Independente", + "value": "independente" + }, + { + "label": "Independiente", + "value": "independiente" + }, + { + "label": "Inocente", + "value": "inocente" + }, + { + "label": "jefe", + "value": "jefe" + }, + { + "label": "Jefe / CEO", + "value": "jefe-ceo" + }, + { + "label": "kicking", + "value": "kicking" + }, + { + "label": "king", + "value": "king" + }, + { + "label": "legend", + "value": "legend" + }, + { + "label": "Literature", + "value": "literature" + }, + { + "label": "loser", + "value": "loser" + }, + { + "label": "Love", + "value": "love" + }, + { + "label": "Love & Culture", + "value": "love-culture" + }, + { + "label": "love after marriage", + "value": "love-after-marriage" + }, + { + "label": "love story", + "value": "love-story" + }, + { + "label": "LOVEAFTERMARRIAGE", + "value": "loveaftermarriage" + }, + { + "label": "lucky dog", + "value": "lucky-dog" + }, + { + "label": "Lugar para você Allnovelread", + "value": "lugar-para-voce-allnovelread" + }, + { + "label": "luna", + "value": "luna" + }, + { + "label": "Madre soltera", + "value": "madre-soltera" + }, + { + "label": "Mafia", + "value": "mafia" + }, + { + "label": "magical world", + "value": "magical-world" + }, + { + "label": "Malentendido", + "value": "malentendido" + }, + { + "label": "Maquinación", + "value": "maquinacion" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Matrimonio", + "value": "matrimonio" + }, + { + "label": "Matrimonio por Contrato", + "value": "matrimonio-por-contrato" + }, + { + "label": "Matrimonio relámpago", + "value": "matrimonio-relampago" + }, + { + "label": "Medico", + "value": "medico" + }, + { + "label": "Médico/Médica", + "value": "medico-medica" + }, + { + "label": "millonaria", + "value": "millonaria" + }, + { + "label": "modificación", + "value": "modificacion" + }, + { + "label": "most millions", + "value": "most-millions" + }, + { + "label": "Mucama", + "value": "mucama" + }, + { + "label": "Mujer súper poderosa", + "value": "mujer-super-poderosa" + }, + { + "label": "Multi-Millionairo", + "value": "multi-millionairo" + }, + { + "label": "Multimillionairo", + "value": "multimillionairo" + }, + { + "label": "Multimillonaria", + "value": "multimillonaria" + }, + { + "label": "multimillonario", + "value": "multimillonario" + }, + { + "label": "Multimillonario-novela", + "value": "multimillonario-novela" + }, + { + "label": "MULTIPLEIDENTITIES", + "value": "multipleidentities" + }, + { + "label": "Múltiples identidades", + "value": "multiples-identidades" + }, + { + "label": "musculoso", + "value": "musculoso" + }, + { + "label": "Nacimiento múltiple", + "value": "nacimiento-multiple" + }, + { + "label": "Novia embarazada a la fuga", + "value": "novia-embarazada-a-la-fuga" + }, + { + "label": "Obsesión", + "value": "obsesion" + }, + { + "label": "Ocultar", + "value": "ocultar" + }, + { + "label": "Optimista", + "value": "optimista" + }, + { + "label": "others", + "value": "others" + }, + { + "label": "Pasión de una noche", + "value": "pasion-de-una-noche" + }, + { + "label": "Perao/Segunda chance", + "value": "perao-segunda-chance" + }, + { + "label": "Perdedor", + "value": "perdedor" + }, + { + "label": "Playboy", + "value": "playboy" + }, + { + "label": "poderoso", + "value": "poderoso" + }, + { + "label": "polygamy", + "value": "polygamy" + }, + { + "label": "Posesivo", + "value": "posesivo" + }, + { + "label": "possessive", + "value": "possessive" + }, + { + "label": "Possessivo", + "value": "possessivo" + }, + { + "label": "Powerful", + "value": "powerful" + }, + { + "label": "presente", + "value": "presente" + }, + { + "label": "Presidente", + "value": "presidente" + }, + { + "label": "princess", + "value": "princess" + }, + { + "label": "Protective", + "value": "protective" + }, + { + "label": "Protectormadre soltera", + "value": "protectormadre-soltera" + }, + { + "label": "Reconquistar a mi pareja", + "value": "reconquistar-a-mi-pareja" + }, + { + "label": "rejected", + "value": "rejected" + }, + { + "label": "relación", + "value": "relacion" + }, + { + "label": "relationship", + "value": "relationship" + }, + { + "label": "Renacido", + "value": "renacido" + }, + { + "label": "Rey/Reina", + "value": "rey-reina" + }, + { + "label": "Rich", + "value": "rich" + }, + { + "label": "Rico", + "value": "rico" + }, + { + "label": "Ricos", + "value": "ricos" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "romance caliente", + "value": "romance-caliente" + }, + { + "label": "Romance/Romântico", + "value": "romance-romantico" + }, + { + "label": "Romántic", + "value": "romantic" + }, + { + "label": "Romantica", + "value": "romantica" + }, + { + "label": "Romanticas", + "value": "romanticas" + }, + { + "label": "Romantico", + "value": "romantico" + }, + { + "label": "Secretos", + "value": "secretos" + }, + { + "label": "secrets", + "value": "secrets" + }, + { + "label": "seductive", + "value": "seductive" + }, + { + "label": "Segunda Chance", + "value": "segunda-chance" + }, + { + "label": "Segunda oportunidad", + "value": "segunda-oportunidad" + }, + { + "label": "STRONGFEMALELEAD", + "value": "strongfemalelead" + }, + { + "label": "Subrogación", + "value": "subrogacion" + }, + { + "label": "Suspense", + "value": "suspense" + }, + { + "label": "Sweet", + "value": "sweet" + }, + { + "label": "SWEETLOVE", + "value": "sweetlove" + }, + { + "label": "Teenager", + "value": "teenager" + }, + { + "label": "Tierno", + "value": "tierno" + }, + { + "label": "Tragedia", + "value": "tragedia" + }, + { + "label": "Traición", + "value": "traicion" + }, + { + "label": "TraiciónReconquistar a mi pareja", + "value": "traicionreconquistar-a-mi-pareja" + }, + { + "label": "Triángulo amoroso", + "value": "triangulo-amoroso" + }, + { + "label": "Trillizos", + "value": "trillizos" + }, + { + "label": "Trio", + "value": "trio" + }, + { + "label": "Una noche de pasion", + "value": "una-noche-de-pasion" + }, + { + "label": "Universidad", + "value": "universidad" + }, + { + "label": "Valente", + "value": "valente" + }, + { + "label": "Valiente", + "value": "valiente" + }, + { + "label": "Venanza", + "value": "venanza" + }, + { + "label": "Werewolf", + "value": "werewolf" + }, + { + "label": "Ya", + "value": "ya" + }, + { + "label": "Youth", + "value": "youth" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "16+", + "value": "16" + }, + { + "label": "alfa", + "value": "alfa" + }, + { + "label": "Allnovelread Sin vuelta atrás", + "value": "allnovelread-sin-vuelta-atras" + }, + { + "label": "Alpha", + "value": "alpha" + }, + { + "label": "Amor dulce", + "value": "amor-dulce" + }, + { + "label": "Amor y odio", + "value": "amor-y-odio" + }, + { + "label": "Arrogante-novela", + "value": "arrogante-novela" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "Billonario", + "value": "billonario" + }, + { + "label": "bxg", + "value": "bxg" + }, + { + "label": "CEO", + "value": "ceo" + }, + { + "label": "Contemporâneo", + "value": "contemporaneo" + }, + { + "label": "Contract marriage", + "value": "contract-marriage" + }, + { + "label": "crecimiento-del-personaje-novela", + "value": "crecimiento-del-personaje-novela" + }, + { + "label": "Divorce", + "value": "divorce" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "dulce", + "value": "dulce" + }, + { + "label": "El incesante acoso de mi ex marido", + "value": "el-incesante-acoso-de-mi-ex-marido" + }, + { + "label": "Enganar al mejor amigo de mi novio", + "value": "enganar-al-mejor-amigo-de-mi-novio" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "HE", + "value": "he" + }, + { + "label": "heterosexual", + "value": "heterosexual" + }, + { + "label": "Historia-triste-novela", + "value": "historia-triste-novela" + }, + { + "label": "Hombre lobo", + "value": "hombre-lobo" + }, + { + "label": "Hot Romance", + "value": "hot-romance" + }, + { + "label": "Independiente", + "value": "independiente" + }, + { + "label": "Inocente", + "value": "inocente" + }, + { + "label": "king", + "value": "king" + }, + { + "label": "Love", + "value": "love" + }, + { + "label": "love after marriage", + "value": "love-after-marriage" + }, + { + "label": "Luna", + "value": "luna" + }, + { + "label": "Magical world", + "value": "magical-world" + }, + { + "label": "millonaria", + "value": "millonaria" + }, + { + "label": "Multi-Millionaire", + "value": "multi-millionaire" + }, + { + "label": "Multimillionairo", + "value": "multimillionairo" + }, + { + "label": "Multimillonario", + "value": "multimillonario" + }, + { + "label": "Nunca Longe Allnovelread", + "value": "nunca-longe-allnovelread" + }, + { + "label": "Posesivo", + "value": "posesivo" + }, + { + "label": "Querida ex esposa", + "value": "querida-ex-esposa" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romane", + "value": "romane" + }, + { + "label": "Romántica", + "value": "romantica" + }, + { + "label": "Romanticas", + "value": "romanticas" + }, + { + "label": "Romantico", + "value": "romantico" + }, + { + "label": "Sweet", + "value": "sweet" + }, + { + "label": "SWEETLOVE", + "value": "sweetlove" + }, + { + "label": "Te Quero de Volta Allnovelread", + "value": "te-quero-de-volta-allnovelread" + }, + { + "label": "Traicion en altar", + "value": "traicion-en-altar" + }, + { + "label": "Uma Ferida Que Nunca Se Cura Allnovelread", + "value": "uma-ferida-que-nunca-se-cura-allnovelread" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urban/Realistic", + "value": "urban-realistic" + }, + { + "label": "vuelva a mí", + "value": "vuelva-a-mi" + }, + { + "label": "Werewolf", + "value": "werewolf" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/bacalightnovel.json b/scripts/multisrc/lightnovelwp/filters/bacalightnovel.json new file mode 100644 index 0000000..3a27752 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/bacalightnovel.json @@ -0,0 +1,78 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { "label": "Action", "value": "action" }, + { "label": "Adventure", "value": "adventure" }, + { "label": "Comedy", "value": "comedy" }, + { "label": "Drama", "value": "drama" }, + { "label": "Ecchi", "value": "ecchi" }, + { "label": "Fantasy", "value": "fantasy" }, + { "label": "Gender Bender", "value": "gender-bender" }, + { "label": "Harem", "value": "harem" }, + { "label": "Historical", "value": "historical" }, + { "label": "Horror", "value": "horror" }, + { "label": "Isekai", "value": "isekai" }, + { "label": "Josei", "value": "josei" }, + { "label": "Martial Arts", "value": "martial-arts" }, + { "label": "Mature", "value": "mature" }, + { "label": "Mecha", "value": "mecha" }, + { "label": "Mystery", "value": "mystery" }, + { "label": "Psychological", "value": "psychological" }, + { "label": "Romance", "value": "romance" }, + { "label": "School Life", "value": "school-life" }, + { "label": "Sci-fi", "value": "sci-fi" }, + { "label": "Seinen", "value": "seinen" }, + { "label": "Shoujo", "value": "shoujo" }, + { "label": "Shoujo Ai", "value": "shoujo-ai" }, + { "label": "Shounen", "value": "shounen" }, + { "label": "Shounen Ai", "value": "shounen-ai" }, + { "label": "Slice of Life", "value": "slice-of-life" }, + { "label": "Sports", "value": "sports" }, + { "label": "Supernatural", "value": "supernatural" }, + { "label": "Tragedy", "value": "tragedy" }, + { "label": "Wuxia", "value": "wuxia" }, + { "label": "Xianxia", "value": "xianxia" }, + { "label": "Xuanhuan", "value": "xuanhuan" }, + { "label": "Yaoi", "value": "yaoi" }, + { "label": "Yuri", "value": "yuri" } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tipe", + "value": [], + "options": [ + { "label": "", "value": "" }, + { "label": "Novel", "value": "novel" } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { "label": "All", "value": "" }, + { "label": "Ongoing", "value": "ongoing" }, + { "label": "Hiatus", "value": "hiatus" }, + { "label": "Completed", "value": "completed" } + ] + }, + "order": { + "type": "Urutkan", + "label": "Order", + "value": "", + "options": [ + { "label": "Default", "value": "" }, + { "label": "A-Z", "value": "title" }, + { "label": "Z-A", "value": "titlereverse" }, + { "label": "Baru diperbarui", "value": "update" }, + { "label": "Baru ditambahkan", "value": "latest" }, + { "label": "Terpopuler", "value": "popular" } + ] + } + } +} diff --git a/scripts/multisrc/lightnovelwp/filters/centralnovel.json b/scripts/multisrc/lightnovelwp/filters/centralnovel.json new file mode 100644 index 0000000..dc0aa35 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/centralnovel.json @@ -0,0 +1,300 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Gênero", + "value": [], + "options": [ + { + "label": "Ação", + "value": "acao" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adulto", + "value": "adulto" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Artes Marciais", + "value": "artes-marciais" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Comédia", + "value": "comedia" + }, + { + "label": "Cotidiano", + "value": "cotidiano" + }, + { + "label": "Cultivo", + "value": "cultivo" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Escolar", + "value": "escolar" + }, + { + "label": "Esportes", + "value": "esportes" + }, + { + "label": "Evolução", + "value": "evolucao" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Ficção Científica", + "value": "ficcao-cientifica" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harém", + "value": "harem" + }, + { + "label": "Histórico", + "value": "historico" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Magia", + "value": "magia" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medieval", + "value": "medieval" + }, + { + "label": "Mistério", + "value": "misterio" + }, + { + "label": "Mitologia", + "value": "mitologia" + }, + { + "label": "Monstros", + "value": "monstros" + }, + { + "label": "Pet", + "value": "pet" + }, + { + "label": "Protagonista Feminina", + "value": "protagonista-feminina" + }, + { + "label": "Protagonista Maligno", + "value": "protagonista-maligno" + }, + { + "label": "Psicológico", + "value": "psicologico" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reencarnação", + "value": "reencarnacao" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen BL", + "value": "shounen-bl" + }, + { + "label": "Sistema", + "value": "sistema" + }, + { + "label": "Sistema de Jogo", + "value": "sistema-de-jogo" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sobrenatural", + "value": "sobrenatural" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Terror", + "value": "terror" + }, + { + "label": "Tragédia", + "value": "tragedia" + }, + { + "label": "Transmigração", + "value": "transmigracao" + }, + { + "label": "Vida Escolar", + "value": "vida-escolar" + }, + { + "label": "VRMMO", + "value": "vrmmo" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tipo", + "value": [], + "options": [ + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Novel Chinesa", + "value": "novel-chinesa" + }, + { + "label": "Novel Coreana", + "value": "novel-coreana" + }, + { + "label": "Novel Japonesa", + "value": "novel-japonesa" + }, + { + "label": "Novel Ocidental", + "value": "novel-ocidental" + }, + { + "label": "Webnovel", + "value": "webnovel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "Todos", + "value": "" + }, + { + "label": "Em andamento", + "value": "em andamento" + }, + { + "label": "Hiato", + "value": "hiato" + }, + { + "label": "Completo", + "value": "completo" + } + ] + }, + "order": { + "type": "Picker", + "label": "Ordenar por", + "value": "", + "options": [ + { + "label": "Padrão", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Últ. Att", + "value": "update" + }, + { + "label": "Últ. Add", + "value": "latest" + }, + { + "label": "Populares", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/daotranslate.json b/scripts/multisrc/lightnovelwp/filters/daotranslate.json new file mode 100644 index 0000000..892bd3e --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/daotranslate.json @@ -0,0 +1,308 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary", + "value": "contemporary" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Family", + "value": "family" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Growth", + "value": "growth" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hidden Identity", + "value": "hidden-identity" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Overpowered", + "value": "overpowered" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Rich", + "value": "rich" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Son-in-Law", + "value": "son-in-law" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Superpower", + "value": "superpower" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Virtual Game", + "value": "virtual-game" + }, + { + "label": "Warrior", + "value": "warrior" + }, + { + "label": "Webtoons", + "value": "webtoons" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Light Novel (KR)", + "value": "light-novel-kr" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/genesistls.json b/scripts/multisrc/lightnovelwp/filters/genesistls.json new file mode 100644 index 0000000..0295089 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/genesistls.json @@ -0,0 +1,240 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Academy", + "value": "academy" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Another World", + "value": "another-world" + }, + { + "label": "Comdey", + "value": "comdey" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Dark Fantasy", + "value": "dark-fantasy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Fusion", + "value": "fantasy-fusion" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Misunderstanding", + "value": "misunderstanding" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Modern Fantasy", + "value": "modern-fantasy" + }, + { + "label": "Munchkin", + "value": "munchkin" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "mystery", + "value": "mystery" + }, + { + "label": "No Harem", + "value": "no-harem" + }, + { + "label": "NO NTR", + "value": "no-ntr" + }, + { + "label": "obsession", + "value": "obsession" + }, + { + "label": "Possession", + "value": "possession" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Regret", + "value": "regret" + }, + { + "label": "reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Slice of life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigrated to Game", + "value": "transmigrated-to-game" + }, + { + "label": "Transmigration", + "value": "transmigration" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "korean novel", + "value": "korean-novel" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/knoxt.json b/scripts/multisrc/lightnovelwp/filters/knoxt.json new file mode 100644 index 0000000..f94ba0d --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/knoxt.json @@ -0,0 +1,424 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Ancient times", + "value": "ancient-times" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Campus", + "value": "campus" + }, + { + "label": "carefree protagonist", + "value": "carefree-protagonist" + }, + { + "label": "celebrity", + "value": "celebrity" + }, + { + "label": "Chinese novel", + "value": "chinese-novel" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Entertaiment circle", + "value": "entertaiment-circle" + }, + { + "label": "Entertainment circle", + "value": "entertainment-circle" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fiction", + "value": "fiction" + }, + { + "label": "futuristic setting", + "value": "futuristic-setting" + }, + { + "label": "Gaming", + "value": "gaming" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "General", + "value": "general" + }, + { + "label": "GL", + "value": "gl" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "historic love", + "value": "historic-love" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "humor", + "value": "humor" + }, + { + "label": "Idol", + "value": "idol" + }, + { + "label": "infrastructure", + "value": "infrastructure" + }, + { + "label": "interstellar", + "value": "interstellar" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "love at first sight", + "value": "love-at-first-sight" + }, + { + "label": "love romance", + "value": "love-romance" + }, + { + "label": "Male protagonist", + "value": "male-protagonist" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mpreg", + "value": "mpreg" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Omegaverse", + "value": "omegaverse" + }, + { + "label": "Otherworld fantasy", + "value": "otherworld-fantasy" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Quick transmigration", + "value": "quick-transmigration" + }, + { + "label": "REBIRTH", + "value": "rebirth" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "shonen ai", + "value": "shonen-ai" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen ai", + "value": "shounen-ai" + }, + { + "label": "Showbi", + "value": "showbi" + }, + { + "label": "showbiz", + "value": "showbiz" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "suspense", + "value": "suspense" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Unlimited flow", + "value": "unlimited-flow" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Western", + "value": "western" + }, + { + "label": "Wu xia", + "value": "wu-xia" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "chinese", + "value": "chinese" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "historic love", + "value": "historic-love" + }, + { + "label": "Japanese Novel", + "value": "japanese-novel" + }, + { + "label": "Kō Randō (藍銅 紅)", + "value": "ko-rando-藍銅-紅" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Light Novel (CN)", + "value": "light-novel-cn" + }, + { + "label": "Original Novel", + "value": "original-novel" + }, + { + "label": "Published Novel", + "value": "published-novel" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Quick Transmigration", + "value": "quick-transmigration" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "Short Story", + "value": "short-story" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/kolnovel.json b/scripts/multisrc/lightnovelwp/filters/kolnovel.json new file mode 100644 index 0000000..152e5e4 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/kolnovel.json @@ -0,0 +1,268 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "تصنيف", + "value": [], + "options": [ + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "XUANHUAN", + "value": "xuanhuan" + }, + { + "label": "أكشن", + "value": "action" + }, + { + "label": "إثارة", + "value": "excitement" + }, + { + "label": "إنتقال الى عالم أخر", + "value": "isekai" + }, + { + "label": "إيتشي", + "value": "etchi" + }, + { + "label": "الخيال العلمي", + "value": "sci-fi" + }, + { + "label": "بوليسي", + "value": "policy" + }, + { + "label": "تاريخي", + "value": "historical" + }, + { + "label": "تحقيقات", + "value": "تحقيق" + }, + { + "label": "تقمص شخصيات", + "value": "rpg" + }, + { + "label": "جريمة", + "value": "crime" + }, + { + "label": "جوسى", + "value": "josei" + }, + { + "label": "جوسي", + "value": "جوسي" + }, + { + "label": "حريم", + "value": "harem" + }, + { + "label": "حياة مدرسية", + "value": "school-life" + }, + { + "label": "خيالي(فانتازيا)", + "value": "fantasy" + }, + { + "label": "دراما", + "value": "drama" + }, + { + "label": "رعب", + "value": "horror" + }, + { + "label": "رومانسي", + "value": "romantic" + }, + { + "label": "سحر", + "value": "magic" + }, + { + "label": "سينن", + "value": "senen" + }, + { + "label": "شريحة من الحياة", + "value": "slice-of-life" + }, + { + "label": "شوجو", + "value": "shojo" + }, + { + "label": "شونين", + "value": "shonen" + }, + { + "label": "طبي", + "value": "medical" + }, + { + "label": "ظواهر خارقة للطبيعة", + "value": "supernatural" + }, + { + "label": "غموض", + "value": "mysteries" + }, + { + "label": "فانتازيا", + "value": "فانتازيا" + }, + { + "label": "فنون القتال", + "value": "martial-arts" + }, + { + "label": "قصة قصيرة", + "value": "قصة-قصيرة" + }, + { + "label": "قوى خارقة", + "value": "superpower" + }, + { + "label": "كوميدي", + "value": "comedy" + }, + { + "label": "مأساوي", + "value": "tragedy" + }, + { + "label": "ما بعد الكارثة", + "value": "after-the-disaster" + }, + { + "label": "مغامرة", + "value": "adventure" + }, + { + "label": "ميكا", + "value": "mechanical" + }, + { + "label": "ناضج", + "value": "mature" + }, + { + "label": "نفسي", + "value": "psychological" + }, + { + "label": "ون شوت", + "value": "ون-شوت" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "النوع", + "value": [], + "options": [ + { + "label": "إنجليزية", + "value": "english" + }, + { + "label": "رواية لايت", + "value": "light-novel" + }, + { + "label": "رواية ويب", + "value": "web-novel" + }, + { + "label": "صينية", + "value": "chinese" + }, + { + "label": "عربية", + "value": "arabic" + }, + { + "label": "كورية", + "value": "korean" + }, + { + "label": "ون شوت", + "value": "ون-شوت" + }, + { + "label": "يابانية", + "value": "japanese" + } + ] + }, + "status": { + "type": "Picker", + "label": "الحالة", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "ترتيب حسب", + "value": "", + "options": [ + { + "label": "الإعداد الأولي", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "أخر التحديثات", + "value": "update" + }, + { + "label": "أخر ما تم إضافته", + "value": "latest" + }, + { + "label": "الرائجة", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/lightnovelfr.json b/scripts/multisrc/lightnovelwp/filters/lightnovelfr.json new file mode 100644 index 0000000..0433034 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/lightnovelfr.json @@ -0,0 +1,248 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "action", + "value": "action" + }, + { + "label": "alchimie", + "value": "alchimie" + }, + { + "label": "Arts martiaux", + "value": "arts-martiaux" + }, + { + "label": "Aventure", + "value": "aventure" + }, + { + "label": "Célébrités", + "value": "celebrites" + }, + { + "label": "chinese", + "value": "chinese" + }, + { + "label": "Combat", + "value": "combat" + }, + { + "label": "comedie", + "value": "comedie" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "dark fantasy", + "value": "dark-fantasy" + }, + { + "label": "Demon", + "value": "demon" + }, + { + "label": "Drame", + "value": "drame" + }, + { + "label": "Fantasie", + "value": "fantasie" + }, + { + "label": "Fantastique", + "value": "fantastique" + }, + { + "label": "Guildes", + "value": "guildes" + }, + { + "label": "harem", + "value": "harem" + }, + { + "label": "Idole", + "value": "idole" + }, + { + "label": "japan", + "value": "japan" + }, + { + "label": "jeux vidéos", + "value": "jeux-videos" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "korean", + "value": "korean" + }, + { + "label": "light Novel", + "value": "light-novel" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "mature", + "value": "mature" + }, + { + "label": "Mystère", + "value": "mystere" + }, + { + "label": "novel", + "value": "novel" + }, + { + "label": "Renaissance", + "value": "renaissance" + }, + { + "label": "Roman", + "value": "roman" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "School life", + "value": "school-life" + }, + { + "label": "Science-fiction", + "value": "science-fiction" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shonen", + "value": "shonen" + }, + { + "label": "showbiz", + "value": "showbiz" + }, + { + "label": "Surnaturel", + "value": "surnaturel" + }, + { + "label": "système", + "value": "systeme" + }, + { + "label": "Système de niveau", + "value": "systeme-de-niveau" + }, + { + "label": "tranche de vie", + "value": "tranche-de-vie" + }, + { + "label": "webnovel", + "value": "webnovel" + }, + { + "label": "xp", + "value": "xp" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Light novel", + "value": "light-novel" + }, + { + "label": "novel chinois", + "value": "novel-chinois" + } + ] + }, + "status": { + "type": "Picker", + "label": "Statut", + "value": "", + "options": [ + { + "label": "Tout", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Par ordre", + "value": "", + "options": [ + { + "label": "Par défaut", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Dernier mise à jour", + "value": "update" + }, + { + "label": "Derniers Ajouts", + "value": "latest" + }, + { + "label": "Populaire", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/noblemtl.json b/scripts/multisrc/lightnovelwp/filters/noblemtl.json new file mode 100644 index 0000000..35034bd --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/noblemtl.json @@ -0,0 +1,492 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "A.I", + "value": "a-i" + }, + { + "label": "Academy", + "value": "academy" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Alternative History", + "value": "alternative-history" + }, + { + "label": "Another World", + "value": "another-world" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Bromance", + "value": "bromance" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cthulhu", + "value": "cthulhu" + }, + { + "label": "Dark fantasy", + "value": "dark-fantasy" + }, + { + "label": "Demons", + "value": "demons" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dystopia", + "value": "dystopia" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Entertainment", + "value": "entertainment" + }, + { + "label": "Exhaustion", + "value": "exhaustion" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "fantasy", + "value": "fantasy" + }, + { + "label": "finance", + "value": "finance" + }, + { + "label": "For men", + "value": "for-men" + }, + { + "label": "Full color", + "value": "full-color" + }, + { + "label": "fusion", + "value": "fusion" + }, + { + "label": "gacha", + "value": "gacha" + }, + { + "label": "Gallery", + "value": "gallery" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Genius", + "value": "genius" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Healing", + "value": "healing" + }, + { + "label": "Hero", + "value": "hero" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "korean novel", + "value": "korean-novel" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "List Adventure Manga Genres", + "value": "list-adventure-manga-genres" + }, + { + "label": "Long Strip", + "value": "long-strip" + }, + { + "label": "Love comedy", + "value": "love-comedy" + }, + { + "label": "magic", + "value": "magic" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Medieval", + "value": "medieval" + }, + { + "label": "Middle Ages", + "value": "middle-ages" + }, + { + "label": "Misunderstanding", + "value": "misunderstanding" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "modern fantasy", + "value": "modern-fantasy" + }, + { + "label": "Munchkin", + "value": "munchkin" + }, + { + "label": "music", + "value": "music" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Necromancy", + "value": "necromancy" + }, + { + "label": "No Romance", + "value": "no-romance" + }, + { + "label": "NTL", + "value": "ntl" + }, + { + "label": "o", + "value": "o" + }, + { + "label": "Obsession", + "value": "obsession" + }, + { + "label": "Politics", + "value": "politics" + }, + { + "label": "Possession", + "value": "possession" + }, + { + "label": "Programming", + "value": "programming" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Pure Love", + "value": "pure-love" + }, + { + "label": "reasoning", + "value": "reasoning" + }, + { + "label": "Redemption", + "value": "redemption" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Regret", + "value": "regret" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Return", + "value": "return" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Reversal", + "value": "reversal" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romance Fanrasy", + "value": "romance-fanrasy" + }, + { + "label": "Salvation", + "value": "salvation" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Science fiction", + "value": "science-fiction" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Soft yandere", + "value": "soft-yandere" + }, + { + "label": "Space opera", + "value": "space-opera" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "system", + "value": "system" + }, + { + "label": "Time limit", + "value": "time-limit" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "TRPG", + "value": "trpg" + }, + { + "label": "TS", + "value": "ts" + }, + { + "label": "Tsundere", + "value": "tsundere" + }, + { + "label": "Unique", + "value": "unique" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Villain", + "value": "villain" + }, + { + "label": "Wholesome", + "value": "wholesome" + }, + { + "label": "Wisdom", + "value": "wisdom" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yandere", + "value": "yandere" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese novel", + "value": "chinese-novel" + }, + { + "label": "habyeol", + "value": "habyeol" + }, + { + "label": "korean novel", + "value": "korean-novel" + }, + { + "label": "Web Novel", + "value": "web-novel" + }, + { + "label": "삼심", + "value": "삼심" + }, + { + "label": "호곡", + "value": "호곡" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/novelsknight.json b/scripts/multisrc/lightnovelwp/filters/novelsknight.json new file mode 100644 index 0000000..b01d2bc --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/novelsknight.json @@ -0,0 +1,344 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Fan fiction", + "value": "fan-fiction" + }, + { + "label": "fanqienovel", + "value": "fanqienovel" + }, + { + "label": "fanstay", + "value": "fanstay" + }, + { + "label": "fantasy", + "value": "fantasy" + }, + { + "label": "football", + "value": "football" + }, + { + "label": "horror", + "value": "horror" + }, + { + "label": "Huyen Huyen", + "value": "huyen-huyen" + }, + { + "label": "light novel", + "value": "light-novel" + }, + { + "label": "living", + "value": "living" + }, + { + "label": "martial arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "military", + "value": "military" + }, + { + "label": "military history", + "value": "military-history" + }, + { + "label": "Mystical Demons", + "value": "mystical-demons" + }, + { + "label": "nba", + "value": "nba" + }, + { + "label": "roamnce", + "value": "roamnce" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "sc-fi online game", + "value": "sc-fi-online-game" + }, + { + "label": "Sci-fi online games", + "value": "sci-fi-online-games" + }, + { + "label": "Science fiction", + "value": "science-fiction" + }, + { + "label": "Science fiction online game", + "value": "science-fiction-online-game" + }, + { + "label": "speech", + "value": "speech" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "tanmi doujin", + "value": "tanmi-doujin" + }, + { + "label": "Terrifying supernatural", + "value": "terrifying-supernatural" + }, + { + "label": "urabn", + "value": "urabn" + }, + { + "label": "urban", + "value": "urban" + }, + { + "label": "Urban Fiction", + "value": "urban-fiction" + }, + { + "label": "urban;romance", + "value": "urbanromance" + }, + { + "label": "wuxia", + "value": "wuxia" + }, + { + "label": "wuxia fairy", + "value": "wuxia-fairy" + }, + { + "label": "Xuan Feng weak initiation", + "value": "xuan-feng-weak-initiation" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Youth Campus", + "value": "youth-campus" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Black Basket: Platinum Star", + "value": "black-basket-platinum-star" + }, + { + "label": "ciweimao", + "value": "ciweimao" + }, + { + "label": "Doomsday", + "value": "doomsday" + }, + { + "label": "dragon ball", + "value": "dragon-ball" + }, + { + "label": "Entertainment", + "value": "entertainment" + }, + { + "label": "falo", + "value": "falo" + }, + { + "label": "faloo", + "value": "faloo" + }, + { + "label": "fan fiction", + "value": "fan-fiction" + }, + { + "label": "fanqienovel", + "value": "fanqienovel" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Football", + "value": "football" + }, + { + "label": "Harry Potter", + "value": "harry-potter" + }, + { + "label": "horror", + "value": "horror" + }, + { + "label": "Hunter X Hunter", + "value": "hunter-x-hunter" + }, + { + "label": "Infinite Evolution", + "value": "infinite-evolution" + }, + { + "label": "Lords: Opens with a Bloody Nest", + "value": "lords-opens-with-a-bloody-nest" + }, + { + "label": "martial arts", + "value": "martial-arts" + }, + { + "label": "marvel", + "value": "marvel" + }, + { + "label": "naruto", + "value": "naruto" + }, + { + "label": "nba", + "value": "nba" + }, + { + "label": "nove", + "value": "nove" + }, + { + "label": "novel", + "value": "novel" + }, + { + "label": "one piece", + "value": "one-piece" + }, + { + "label": "online game", + "value": "online-game" + }, + { + "label": "Please", + "value": "please" + }, + { + "label": "qidian", + "value": "qidian" + }, + { + "label": "qimao", + "value": "qimao" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Science-fiction", + "value": "science-fiction" + }, + { + "label": "this is a world of horror", + "value": "this-is-a-world-of-horror" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/novelsparadise.json b/scripts/multisrc/lightnovelwp/filters/novelsparadise.json new file mode 100644 index 0000000..c76fabb --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/novelsparadise.json @@ -0,0 +1,156 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "النوع", + "value": [], + "options": [ + { + "label": "+18", + "value": "18" + }, + { + "label": "آليه", + "value": "آليه" + }, + { + "label": "اكشن", + "value": "اكشن" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رومانسي", + "value": "رومانسي" + }, + { + "label": "سحر", + "value": "سحر" + }, + { + "label": "شبه بشريه", + "value": "شبه-بشريه" + }, + { + "label": "صيني", + "value": "صيني" + }, + { + "label": "غموض", + "value": "غموض" + }, + { + "label": "فان فيكشن", + "value": "فان-فيكشن" + }, + { + "label": "فنون قتاليه", + "value": "فنون-قتاليه" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "مغامره", + "value": "مغامره" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "النوع", + "value": [], + "options": [ + { + "label": "روايات صينيه", + "value": "روايات-صينيه" + }, + { + "label": "روايات كوريه", + "value": "روايات-كوريه" + }, + { + "label": "روايات ويب", + "value": "روايات-ويب" + } + ] + }, + "status": { + "type": "Picker", + "label": "الحالة", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "ترتيب بواسطة", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "أخر التحديثات", + "value": "update" + }, + { + "label": "المضاف حديثاً", + "value": "latest" + }, + { + "label": "شائع", + "value": "popular" + }, + { + "label": "{{advanced_search_series_order_by_rating_label}}", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/noveltr.json b/scripts/multisrc/lightnovelwp/filters/noveltr.json new file mode 100644 index 0000000..f91330f --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/noveltr.json @@ -0,0 +1,252 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Bilim Kurgu", + "value": "bilim-kurgu" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "dövüş sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "ecchi", + "value": "ecchi" + }, + { + "label": "fantastik", + "value": "fantastik" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "gizem", + "value": "gizem" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "korku", + "value": "korku" + }, + { + "label": "macera", + "value": "macera" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "okul", + "value": "okul" + }, + { + "label": "oyun", + "value": "oyun" + }, + { + "label": "psikoloji", + "value": "psikoloji" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "reenkarnasyon", + "value": "reenkarnasyon" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romantik", + "value": "romantik" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "süper kahraman", + "value": "super-kahraman" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "tarih", + "value": "tarih" + }, + { + "label": "trajedi", + "value": "trajedi" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "yetişkin", + "value": "yetiskin" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tür", + "value": [], + "options": [ + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Durum", + "value": "", + "options": [ + { + "label": "Hepsi", + "value": "" + }, + { + "label": "Devam Ediyor", + "value": "ongoing" + }, + { + "label": "Askıda", + "value": "hiatus" + }, + { + "label": "Tamamlanmış", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Sıralama", + "value": "", + "options": [ + { + "label": "Varsayılan", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/pandamtl.json b/scripts/multisrc/lightnovelwp/filters/pandamtl.json new file mode 100644 index 0000000..e75635f --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/pandamtl.json @@ -0,0 +1,156 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Light Novel (KR)", + "value": "light-novel-kr" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/universalnovel.json b/scripts/multisrc/lightnovelwp/filters/universalnovel.json new file mode 100644 index 0000000..8e2b9e7 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/universalnovel.json @@ -0,0 +1,248 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Acting", + "value": "acting" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Bl", + "value": "bl" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "entertainment", + "value": "entertainment" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Ghost", + "value": "ghost" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Male protagonist", + "value": "male-protagonist" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sprited animal", + "value": "sprited-animal" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Unlimited flow", + "value": "unlimited-flow" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/generator.ts b/scripts/multisrc/lightnovelwp/generator.ts new file mode 100644 index 0000000..8be8981 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/generator.ts @@ -0,0 +1,35 @@ +import { ScrpitGeneratorFunction } from "../generate"; +import { LightNovelWPMetadata } from "./template"; +import list from "./sources.json"; +import { readFileSync } from "fs"; +import path from "path"; + +export const generateAll: ScrpitGeneratorFunction = function (name) { + return list.map((source: LightNovelWPMetadata) => { + let filters: any = {}; + try { + filters = require(`./filters/${source.id}`); + source.filters = filters.filters; + } catch (e) {} + console.log(`[${name}] Generating:`, source.id, source.filters? "\t🔎with filters🔍" : "\t🚫no filters🚫"); + return generator(source); + }); +}; + +const generator = function generator(source: LightNovelWPMetadata) { + const LightNovelWPTemplate = readFileSync(path.join(__dirname, "template.ts"),{ + encoding: "utf-8", + }); + + const pluginScript = ` + ${LightNovelWPTemplate} +const plugin = new LightNovelWPPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: source.options?.lang || "English", + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/scripts/multisrc/lightnovelwp/get_filters.ts b/scripts/multisrc/lightnovelwp/get_filters.ts new file mode 100644 index 0000000..8d51920 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/get_filters.ts @@ -0,0 +1,103 @@ +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; + +function getFilters(name: string, html: string) { + const $:cheerio.CheerioAPI = cheerio.load(html); + const filters: any = {filters: { + "genre[]": { + type: "Checkbox", + label: "Genre", + value: [], + options: [] + }, + "type[]": { + type: "Checkbox", + label: "Type", + value: [], + options: [] + }, + "status": { + type: "Picker", + label: "Status", + value: "", + options: [] + }, + "order": { + type: "Picker", + label: "Order", + value: "", + options: [] + }, + }}; + + const filtersContainer = $('div.quickfilter').find("ul"); + filtersContainer.each((i, el) => { + const filterName = Object.keys(filters.filters)[i]; + filters.filters[filterName].label = $(el).prev().contents().first().text().trim(); + $(el).find("li").each((j, li) => { + filters.filters[filterName].options.push({ + label: $(li).text().trim(), + value: decodeURI($(li).find("input").attr("value") || "") + }); + }); + }); + fs.writeFileSync( + path.join(__dirname, 'filters', name + ".json"), + JSON.stringify(filters, null, 2) + ); +} + +async function getFiltersFromURL(name: string, url: string) { + const response = await fetch(url + "/series/"); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}, while fetching ${response.url}`); + } + const html = await response.text(); + try { + getFilters(name, html); + } catch (e) { + console.error("Error while getting filters from", url); + } +} + + +async function askGetFilter(){ + const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout + }); + + const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; + await readline.question('Enter the id of the site (same one as in sources.json): ', async (name: string) => { + await readline.question(EREASE_PREV_LINE + 'Do you want to get the filters from a URL or the html text? (if url dosen\'t work try html) (url/html): ', async (method: string) => { + if (method.toLowerCase() === "url") { + await readline.question(EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ', async (url: string) => { + try { + await getFiltersFromURL(name, url); + } catch (e: any) { + console.error("Error while getting filters from", url); + console.log(e.message || e); + } + readline.close(); + }); + } else { + process.stdout.write(EREASE_PREV_LINE + `Enter the html text from the page at {sourceSite}/series (at the end press ENTER then press CTRL+C) +(to make it faster you can run \`$(\"div.quickfilter\").parent().html()\` in the console to get only the important html part): `) + let html = ""; + readline.on('SIGINT', () => { + console.log("Creating filters file"); + getFilters(name, html); + readline.close(); + }); + readline.on('line', (line: string) => { + html += line + "\n"; + }); + } + }); + }); +} + +askGetFilter(); + +export { getFiltersFromURL }; diff --git a/scripts/multisrc/lightnovelwp/sources.json b/scripts/multisrc/lightnovelwp/sources.json new file mode 100644 index 0000000..c21987d --- /dev/null +++ b/scripts/multisrc/lightnovelwp/sources.json @@ -0,0 +1,146 @@ +[ + { + "id": "lightnovelfr", + "sourceSite": "https://lightnovelfr.com/", + "sourceName": "Ligh Novel FR", + "options": { + "lang": "French", + "reverseChapters": true + } + }, + { + "id": "kolnovel", + "sourceSite": "https://kolnovel.me/", + "sourceName": "ملوك الروايات", + "options": { + "lang": "Arabic", + "reverseChapters": true + } + }, + { + "id": "knoxt", + "sourceSite": "https://knoxt.space/", + "sourceName": "KnoxT", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "bacalightnovel", + "sourceSite": "https://bacalightnovel.co/", + "sourceName": "Baca Light Novel", + "options": { + "lang": "Indonesian", + "reverseChapters": true + } + }, + { + "id": "allnovelread", + "sourceSite": "https://allnovelread.com/", + "sourceName": "AllNovelRead", + "options": { + "lang": "Spanish", + "reverseChapters": true + } + }, + { + "id": "daotranslate", + "sourceSite": "https://daotranslate.com/", + "sourceName": "DaoTranslate", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "novelsparadise", + "sourceSite": "https://novelsparadise.net/", + "sourceName": "جنة الروايات", + "options": { + "lang": "Arabic", + "reverseChapters": true + } + }, + { + "id": "universalnovel", + "sourceSite": "https://universalnovel.com/", + "sourceName": "Universal Novel", + "options": { + "lang": "English", + "reverseChapters": false + } + }, + { + "id": "noveltr", + "sourceSite": "https://noveltr.com/", + "sourceName": "NovelTR", + "options": { + "lang": "Turkish", + "reverseChapters": true + } + }, + { + "id": "noblemtl", + "sourceSite": "https://noblemtl.com/", + "sourceName": "NobleMTL", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "genesistls", + "sourceSite": "https://genesistls.com/", + "sourceName": "GenesisTls", + "options": { + "lang": "English", + "reverseChapters": false + } + }, + { + "id": "pandamtl", + "sourceSite": "https://pandamtl.com/", + "sourceName": "Panda Machine Translations", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "novelsknight", + "sourceSite": "https://novelsknight.com/", + "sourceName": "NovelsKnight", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "centralnovel", + "sourceSite": "https://centralnovel.com/", + "sourceName": "Central Novel", + "options": { + "lang": "Portuguese", + "reverseChapters": true + } + }, + { + "id": "whitemoonlightnovels", + "sourceSite": "https://whitemoonlightnovels.com/", + "sourceName": "White Moonlight Novels", + "options": { + "lang": "English", + "reverseChapters": false + } + }, + { + "id": "systemtranslation", + "sourceSite": "https://systemtranslation.com/", + "sourceName": "System Translation", + "options": { + "lang": "English", + "reverseChapters": true + } + } +] diff --git a/scripts/multisrc/lightnovelwp/template.ts b/scripts/multisrc/lightnovelwp/template.ts new file mode 100644 index 0000000..1fa1745 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/template.ts @@ -0,0 +1,234 @@ +import { CheerioAPI, load } from "cheerio"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { Plugin } from "@typings/plugin"; +import { NovelStatus } from "@libs/novelStatus"; +import { defaultCover } from "@libs/defaultCover"; +import { Filters } from "@libs/filterInputs"; + +interface LightNovelWPOptions { + reverseChapters?: boolean; + lang?: string; +} + +export interface LightNovelWPMetadata { + id: string; + sourceSite: string; + sourceName: string; + options?: LightNovelWPOptions; + filters?: any; +} + +class LightNovelWPPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + options?: LightNovelWPOptions; + filters?: Filters; + + constructor(metadata: LightNovelWPMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/lightnovelwp/${metadata.id}.png`; + this.site = metadata.sourceSite; + this.version = "1.0.3"; + this.options = metadata.options; + this.filters = metadata.filters satisfies Filters; + } + + getHostname(url: string): string { + return url.split("/")[2]; + } + + async getCheerio(url: string, search: boolean): Promise { + const r = await fetchApi(url); + if (!r.ok && search != true) throw new Error("Could not reach site (" + r.status + ") try to open in webview."); + const $ = load(await r.text()); + const title = $("title").text().trim(); + if (this.getHostname(url) != this.getHostname(r.url) || + title == "Bot Verification" || title == "You are being redirected...") + throw new Error("Captcha error, please open in webview"); + + return ($); + } + + parseNovels($: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + + $("div.listupd > article").each((i, elem) => { + const novelName = $(elem).find("h2").text(); + const image = $(elem).find("img"); + const novelUrl = $(elem).find("a").attr("href"); + + if (novelUrl) { + novels.push({ + name: novelName, + cover: image.attr("data-src") || image.attr("src") || defaultCover, + path: novelUrl.replace(this.site, ""), + }); + } + }); + return novels; + } + + async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions ): Promise { + let url = this.site + "/series/?page=" + pageNo; + if (!filters) filters = {}; + if (showLatestNovels) url += "&order=latest"; + for (const key in filters) { + if (typeof filters[key].value === "object") + for (const value of filters[key].value as string[]) + url += `&${key}=${value}`; + else if (filters[key].value) + url += `&${key}=${filters[key].value}`; + } + const $ = await this.getCheerio(url, false); + return this.parseNovels($); + } + + async parseNovel(novelPath: string): Promise { + const $ = await this.getCheerio(this.site + novelPath, false); + + const novel: Plugin.SourceNovel = { path: novelPath.replace(this.site, ""), name: "Untitled" }; + + novel.name = $("h1.entry-title").text().trim(); + const image = $("img.wp-post-image"); + novel.cover = image.attr("data-src") || image.attr("src") || defaultCover; + switch ($("div.sertostat > span").attr("class")?.toLowerCase() || "") { + 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; + } + + let details = $("div.serl"); + if (!details.length) details = $("div.spe > span"); + details.each(function () { + const detailName = $(this).contents().first().text().replace(":", "").trim().toLowerCase(); + const detail = $(this).contents().last().text().trim().toLowerCase(); + + switch (detailName) { + case "الكاتب": + case "author": + case "auteur": + case "autor": + case "yazar": + novel.author = detail; + break; + case "الحالة": + case "status": + case "statut": + case "estado": + case "durum": + switch (detail) { + case "مكتملة": + case "completed": + case "complété": + case "completo": + case "completado": + case "tamamlandı": + novel.status = NovelStatus.Completed; + break; + case "مستمرة": + case "ongoing": + case "en cours": + case "em andamento": + case "en progreso": + case "devam ediyor": + novel.status = NovelStatus.Ongoing; + break; + case "متوقفة": + case "hiatus": + case "en pause": + case "hiato": + case "pausa": + case "pausado": + case "duraklatıldı": + novel.status = NovelStatus.OnHiatus; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + break; + case "الفنان": + case "artist": + case "artiste": + case "artista": + case "çizer": + novel.artist = detail; + break; + } + }); + + let genres = $(".sertogenre") + if (!genres.length) genres = $(".genxed") + novel.genres = genres + .children("a") + .map((i, el) => $(el).text()) + .toArray() + .join(","); + + let summary = $(".sersys > p").map((i, el) => $(el).text().trim()).toArray(); + if (!summary.length) summary = $(".entry-content > p").map((i, el) => $(el).text().trim()).toArray(); + novel.summary = summary.join("\n"); + + const chapters: Plugin.ChapterItem[] = []; + $(".eplister li").each((i, elem) => { + const chapterName = $(elem).find(".epl-num").text() + " " + $(elem).find(".epl-title").text(); + const chapterUrl = $(elem).find("a").attr("href") || ""; + const releaseTime = $(elem).find(".epl-date").text().trim(); + let isFreeChapter: boolean; + switch ($(elem).find(".epl-price").text().trim().toLowerCase()) { + case "free": + case "gratuit": + case "مجاني": + case "livre": + case "": + isFreeChapter = true; + break; + default: + isFreeChapter = false; + break; + } + if (isFreeChapter) + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ""), + releaseTime: releaseTime, + }); + }); + + if (this.options?.reverseChapters && chapters.length) chapters.reverse(); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const $ = await this.getCheerio(this.site + chapterPath, false); + if (this.id == "kolnovel") { + let ignore = $("article > style").text().trim().split(","); + ignore.push(...(ignore.pop()?.match(/^\.\w+/) || [])); + ignore.map((tag) => $(`p${tag}`).remove()); + } + return $(".epcontent p").map((i, el) => $(el)).toArray().join("\n") || ""; + } + + async searchNovels(searchTerm: string, page: number): Promise { + const url = this.site + "page/" + page + "/?s=" + searchTerm; + const $ = await this.getCheerio(url, true); + return this.parseNovels($); + } + + fetchImage = fetchFile; +} diff --git a/scripts/multisrc/madara/template.ts b/scripts/multisrc/madara/template.ts index 5c08ba7..da22355 100644 --- a/scripts/multisrc/madara/template.ts +++ b/scripts/multisrc/madara/template.ts @@ -6,7 +6,6 @@ import { defaultCover } from "@libs/defaultCover"; import { NovelStatus } from "@libs/novelStatus"; import dayjs from "dayjs"; -const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); const includesAny = (str: string, keywords: string[]) => new RegExp(keywords.join("|")).test(str); @@ -38,6 +37,7 @@ export interface MadaraMetadata { options?: MadaraOptions; filters?: Filters; } + class MadaraPlugin implements Plugin.PluginBase { id: string; name: string; @@ -57,6 +57,7 @@ class MadaraPlugin implements Plugin.PluginBase { this.options = metadata.options; this.filters = metadata.filters; } + async popularNovels( pageNo: number, { @@ -69,8 +70,8 @@ class MadaraPlugin implements Plugin.PluginBase { let url = this.site; if (filters?.genres?.value) { url += - (this.options?.path?.genres || MadaraDefaultPath.genres) + - "/" + filters.genres.value; + (this.options?.path?.genres || MadaraDefaultPath.genres) + "/" + + filters.genres.value; } else { url += this.options?.path?.novels || MadaraDefaultPath.novels; } @@ -83,18 +84,19 @@ class MadaraPlugin implements Plugin.PluginBase { loadedCheerio(".manga-title-badges").remove(); - loadedCheerio(".page-item-detail").each(function () { - const novelName = loadedCheerio(this).find(".post-title").text().trim(); - const image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); + loadedCheerio(".page-item-detail").each((index, element) => { + const novelName = loadedCheerio(element).find(".post-title").text().trim(); const novelUrl = - loadedCheerio(this).find(".post-title").find("a").attr("href") || ""; + loadedCheerio(element).find(".post-title").find("a").attr("href") || ""; if (!novelName || !novelUrl) return; + const image = loadedCheerio(element).find("img"); + const novelCover = image.attr("data-src") || image.attr("src"); + const novel: Plugin.NovelItem = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ""), }; novels.push(novel); @@ -102,19 +104,18 @@ class MadaraPlugin implements Plugin.PluginBase { return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const novel: Plugin.SourceNovel = { - url: novelUrl, - }; - - const body = await fetchApi(novelUrl).then((res) => res.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); let loadedCheerio = parseHTML(body); loadedCheerio(".manga-title-badges, #manga-title span").remove(); - novel.name = - loadedCheerio(".post-title h1").text().trim() || - loadedCheerio("#manga-title h1").text(); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: + loadedCheerio(".post-title h1").text().trim() || + loadedCheerio("#manga-title h1").text(), + }; novel.cover = loadedCheerio(".summary_image > a > img").attr("data-lazy-src") || @@ -161,7 +162,7 @@ class MadaraPlugin implements Plugin.PluginBase { let html = ""; if (this.options?.useNewChapterEndpoint) { - html = await fetchApi(novelUrl + "ajax/chapters/", { + html = await fetchApi(this.site + novelPath + "ajax/chapters/", { method: "POST", }).then((res) => res.text()); } else { @@ -203,7 +204,7 @@ class MadaraPlugin implements Plugin.PluginBase { chapters.push({ name: chapterName, - url: chapterUrl, + path: chapterUrl.replace(this.site, ""), releaseTime: releaseDate || null, chapterNumber: totalChapters - chapterIndex, }); @@ -212,8 +213,9 @@ class MadaraPlugin implements Plugin.PluginBase { novel.chapters = chapters.reverse(); return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const chapterText = @@ -225,6 +227,7 @@ class MadaraPlugin implements Plugin.PluginBase { return chapterText; } + async searchNovels( searchTerm: string, pageNo?: number | undefined, @@ -233,21 +236,19 @@ class MadaraPlugin implements Plugin.PluginBase { const url = this.site + "?s=" + searchTerm + "&post_type=wp-manga"; const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = parseHTML(body); - loadedCheerio(".c-tabs-item__content").each(function () { - const novelName = loadedCheerio(this).find(".post-title").text().trim(); - - let image = loadedCheerio(this).find("img"); + loadedCheerio(".c-tabs-item__content").each((index, element) => { + const novelName = loadedCheerio(element).find(".post-title").text().trim(); + const novelUrl = loadedCheerio(element).find(".post-title").find("a").attr("href") || ""; + if (!novelName || !novelUrl) return; + let image = loadedCheerio(element).find("img"); const novelCover = image.attr("data-src") || image.attr("src"); - let novelUrl = - loadedCheerio(this).find(".post-title").find("a").attr("href") || ""; const novel = { name: novelName, cover: novelCover, - url: novelUrl, + path: novelUrl.replace(this.site, ""), }; novels.push(novel); diff --git a/scripts/multisrc/readwn/template.ts b/scripts/multisrc/readwn/template.ts index 60d8b60..a0e7abd 100644 --- a/scripts/multisrc/readwn/template.ts +++ b/scripts/multisrc/readwn/template.ts @@ -39,7 +39,8 @@ class ReadwnPlugin implements Plugin.PluginBase { url += showLatestNovels ? "lastdotime" : filters?.sort?.value || "newstime"; url += "-" + (pageNo - 1) + ".html"; - if (filters?.tags?.value) { //only 1 page + if (filters?.tags?.value) { + //only 1 page url = this.site + "/tags/" + filters.tags.value + "-0.html"; } @@ -48,25 +49,27 @@ class ReadwnPlugin implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = loadedCheerio("li.novel-item") .map((index, element) => ({ - name: loadedCheerio(element).find("h4").text(), - cover: this.site + + name: loadedCheerio(element).find("h4").text() || "", + cover: + this.site + loadedCheerio(element).find(".novel-cover > img").attr("data-src"), - url: this.site + loadedCheerio(element).find("a").attr("href"), + path: loadedCheerio(element).find("a").attr("href") || "", })) - .get(); + .get() + .filter((novel) => novel.name && novel.path); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const body = await fetchApi(novelUrl).then((res) => res.text()); - + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel = { - url: novelUrl, + path: novelPath, + name: loadedCheerio("h1.novel-title").text() || "", }; - novel.name = loadedCheerio("h1.novel-title").text(); novel.author = loadedCheerio("span[itemprop=author]").text(); novel.cover = this.site + loadedCheerio("figure.cover > img").attr("data-src"); @@ -100,11 +103,17 @@ class ReadwnPlugin implements Plugin.PluginBase { const chapters: Plugin.ChapterItem[] = loadedCheerio(".chapter-list li") .map((chapterIndex, element) => { - const name = loadedCheerio(element).find("a .chapter-title").text().trim(); - const url = loadedCheerio(element).find("a").attr("href")?.trim(); - if (!name || !url) return null; + const name = loadedCheerio(element) + .find("a .chapter-title") + .text() + .trim(); + const path = loadedCheerio(element).find("a").attr("href")?.trim(); + if (!name || !path) return null; - let releaseTime = loadedCheerio(element).find("a .chapter-update").text().trim(); + let releaseTime = loadedCheerio(element) + .find("a .chapter-update") + .text() + .trim(); if (releaseTime?.includes?.("ago")) { const timeAgo = releaseTime.match(/\d+/)?.[0] || "0"; const timeAgoInt = parseInt(timeAgo, 10); @@ -112,14 +121,14 @@ class ReadwnPlugin implements Plugin.PluginBase { if (timeAgoInt) { const dayJSDate = dayjs(); // today if ( - releaseTime.includes("hours ago") || + releaseTime.includes("hours ago") || releaseTime.includes("hour ago") ) { dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours } if ( - releaseTime.includes("days ago") || + releaseTime.includes("days ago") || releaseTime.includes("day ago") ) { dayJSDate.subtract(timeAgoInt, "days"); // go back N days @@ -138,7 +147,7 @@ class ReadwnPlugin implements Plugin.PluginBase { return { name, - url: this.site + url, + path, releaseTime, chapterNumber: chapterIndex + 1, }; @@ -148,14 +157,14 @@ class ReadwnPlugin implements Plugin.PluginBase { if (latestChapterNo > chapters.length) { const lastChapterNo = parseInt( - chapters[chapters.length - 1].url.match(/_(\d+)\.html/)?.[1] || "", + chapters[chapters.length - 1].path.match(/_(\d+)\.html/)?.[1] || "", 10, ); for (let i = (lastChapterNo || chapters.length) + 1; i <= latestChapterNo; i++) { chapters.push({ name: "Chapter " + i, - url: novelUrl.replace(".html", "_" + i + ".html"), + path: novelPath.replace(".html", "_" + i + ".html"), releaseTime: null, chapterNumber: i, }); @@ -166,8 +175,8 @@ class ReadwnPlugin implements Plugin.PluginBase { return novel; } - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); const loadedCheerio = parseHTML(body); const chapterText = loadedCheerio(".chapter-content").html() || ""; @@ -190,11 +199,12 @@ class ReadwnPlugin implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = loadedCheerio("li.novel-item") .map((index, element) => ({ - name: loadedCheerio(element).find("h4").text(), + name: loadedCheerio(element).find("h4").text() || "", cover: this.site + loadedCheerio(element).find("img").attr("data-src"), - url: this.site + loadedCheerio(element).find("a").attr("href"), + path: loadedCheerio(element).find("a").attr("href") || "", })) - .get(); + .get() + .filter((novel) => novel.name && novel.path); return novels; } diff --git a/scripts/multisrc/rulate/template.ts b/scripts/multisrc/rulate/template.ts index 15bd5e9..de185be 100644 --- a/scripts/multisrc/rulate/template.ts +++ b/scripts/multisrc/rulate/template.ts @@ -62,23 +62,20 @@ class RulatePlugin implements Plugin.PluginBase { loadedCheerio(element).find("p > a").text(); const name = loadedCheerio(element).find("p > a").text(); const cover = loadedCheerio(element).find("img").attr("src"); - const url = loadedCheerio(element).find("p > a").attr("href"); - if (!name || !url) return; + const path = loadedCheerio(element).find("p > a").attr("href"); + if (!name || !path) return; - novels.push({ name, cover: this.site + cover, url: this.site + url }); + novels.push({ name, cover: this.site + cover, path }); }); return novels; } - async parseNovelAndChapters(novelUrl: string): Promise { - const novel: Plugin.SourceNovel = { - url: novelUrl, - }; - let result = await fetchApi(novelUrl); + async parseNovel(novelPath: string): Promise { + let result = await fetchApi(this.site + novelPath); if (result.url.includes("mature?path=")) { const formData = new FormData(); - formData.append("path", novelUrl); + formData.append("path", novelPath); formData.append("ok", "Да"); await fetchApi(result.url, { @@ -86,23 +83,27 @@ class RulatePlugin implements Plugin.PluginBase { body: formData, }); - result = await fetchApi(novelUrl); + result = await fetchApi(this.site + novelPath); } const body = await result.text(); const loadedCheerio = parseHTML(body); - novel.name = loadedCheerio( - 'div[class="container"] > div > div > h1, div.span8:nth-child(1) > h1:nth-child(2)', - ) - .text() - .trim(); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio( + 'div[class="container"] > div > div > h1, div.span8:nth-child(1) > h1:nth-child(2)', + ) + .text() + .trim(), + }; if (novel.name?.includes?.("[")) { novel.name = novel.name.split("[")[0].trim(); } novel.cover = this.site + loadedCheerio('div[class="images"] > div img').attr("src"); - novel.summary = - loadedCheerio("#Info > div:nth-child(3), .book-description").text().trim(); + novel.summary = loadedCheerio("#Info > div:nth-child(3), .book-description") + .text() + .trim(); novel.author = loadedCheerio( ".book-stats-icons_author > span:nth-child(2) > a:nth-child(1)", ).text(); @@ -143,27 +144,41 @@ class RulatePlugin implements Plugin.PluginBase { } const chapters: Plugin.ChapterItem[] = []; - loadedCheerio("table > tbody > tr.chapter_row").each((chapterIndex, element) => { - const chapterName = loadedCheerio(element).find('td[class="t"] > a').text().trim(); - const releaseDate = loadedCheerio(element).find("td > span").attr("title")?.trim(); - const chapterUrl = loadedCheerio(element).find('td[class="t"] > a').attr("href"); + loadedCheerio("table > tbody > tr.chapter_row").each( + (chapterIndex, element) => { + const chapterName = loadedCheerio(element) + .find('td[class="t"] > a') + .text() + .trim(); + const releaseDate = loadedCheerio(element) + .find("td > span") + .attr("title") + ?.trim(); + const chapterUrl = loadedCheerio(element) + .find('td[class="t"] > a') + .attr("href"); - if (!loadedCheerio(element).find('td > span[class="disabled"]').length && releaseDate) { - chapters.push({ - name: chapterName, - url: this.site + chapterUrl, - releaseTime: this.parseDate(releaseDate), - chapterNumber: chapterIndex + 1, - }); - } - }); + if ( + !loadedCheerio(element).find('td > span[class="disabled"]').length && + releaseDate && + chapterUrl + ) { + chapters.push({ + name: chapterName, + path: chapterUrl, + releaseTime: this.parseDate(releaseDate), + chapterNumber: chapterIndex + 1, + }); + } + }, + ); novel.chapters = chapters; return novel; } - async parseChapter(chapterUrl: string): Promise { - let result = await fetchApi(chapterUrl); + async parseChapter(chapterPath: string): Promise { + let result = await fetchApi(this.site + chapterPath); if (result.url.includes("mature?path=")) { const formData = new FormData(); formData.append("ok", "Да"); @@ -173,7 +188,7 @@ class RulatePlugin implements Plugin.PluginBase { body: formData, }); - result = await fetchApi(chapterUrl); + result = await fetchApi(this.site + chapterPath); } const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -203,7 +218,7 @@ class RulatePlugin implements Plugin.PluginBase { novels.push({ name, cover: this.site + novel.img, - url: this.site + novel.url, + path: novel.url, }); }); diff --git a/scripts/multisrc/wpmangastream/generator.ts b/scripts/multisrc/wpmangastream/generator.ts deleted file mode 100644 index d3089c6..0000000 --- a/scripts/multisrc/wpmangastream/generator.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ScrpitGeneratorFunction } from "../generate"; -import { WPmangaStreamMetadata } from "./template"; -import list from "./sources.json"; -import { readFileSync } from "fs"; -import path from "path"; - -export const generateAll: ScrpitGeneratorFunction = function (name) { - return list.map((source) => { - console.log(`[${name}] Generating:`, source.id); - return generator(source); - }); -}; - -const generator = function generator(source: WPmangaStreamMetadata) { - const WPmangaStreamTemplate = readFileSync(path.join(__dirname, "template.ts"),{ - encoding: "utf-8", - }); - - const pluginScript = ` - ${WPmangaStreamTemplate} -const plugin = new WPmangaStreamPlugin(${JSON.stringify(source)}); -export default plugin; - `.trim(); - - return { - lang: source.options?.lang || "English", - filename: source.sourceName, - pluginScript, - }; -}; diff --git a/scripts/multisrc/wpmangastream/sources.json b/scripts/multisrc/wpmangastream/sources.json deleted file mode 100644 index 0a5fdaa..0000000 --- a/scripts/multisrc/wpmangastream/sources.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "id": "lightnovelfr", - "sourceSite": "https://lightnovelfr.com/", - "sourceName": "Ligh Novel FR", - "options": { - "lang": "French", - "reverseChapters": true - } - }, - { - "id": "kol-novel", - "sourceSite": "https://kolnovel.com/", - "sourceName": "Kol Novel", - "options": { - "lang": "arabic", - "reverseChapters": true - } - } -] diff --git a/scripts/multisrc/wpmangastream/template.ts b/scripts/multisrc/wpmangastream/template.ts deleted file mode 100644 index ea4a02d..0000000 --- a/scripts/multisrc/wpmangastream/template.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { CheerioAPI, load } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; - -interface WPmangaStreamOptions { - reverseChapters?: boolean; - lang?: string; -} - -export interface WPmangaStreamMetadata { - id: string; - sourceSite: string; - sourceName: string; - options?: WPmangaStreamOptions; -} - -class WPmangaStreamPlugin implements Plugin.PluginBase { - id: string; - name: string; - icon: string; - site: string; - version: string; - options?: WPmangaStreamOptions; - - constructor(metadata: WPmangaStreamMetadata) { - this.id = metadata.id; - this.name = metadata.sourceName; - this.icon = `multisrc/wpmangastream/icons/${metadata.id}.png`; - this.site = metadata.sourceSite; - this.version = "1.0.0"; - this.options = metadata.options; - } - - async popularNovels(page: number): Promise { - const url = this.site + "series/?page=" + page + "&status=&order=popular"; - const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = load(body); - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.maindet").each(function () { - const name = loadedCheerio(this).find("h2").text(); - const image = loadedCheerio(this).find("img"); - const url = loadedCheerio(this).find("h2 a").attr("href"); - - if (!url) return; - novels.push({ - name, - cover: image.attr("data-src") || image.attr("src"), - url, - }); - }); - - return novels; - } - - async parseNovelAndChapters(url: string): Promise { - const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = load(body); - - const novel: Plugin.SourceNovel = { url }; - - // novel.url = url; - novel.name = loadedCheerio("h1.entry-title").text(); - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - novel.status = loadedCheerio("div.sertostat > span").attr("class"); - - loadedCheerio("div.serl > span").each(function () { - const detailName = loadedCheerio(this).text().trim(); - const detail = loadedCheerio(this).next().text().trim(); - - switch (detailName) { - case "الكاتب": - case "Author": - case "Auteur": - novel.author = detail; - break; - } - }); - - novel.genres = loadedCheerio(".sertogenre") - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - let summary = loadedCheerio(".sersys > p").siblings().remove("div").end(); - novel.summary = ""; - for (let i = 0; i < summary.length; i++) { - const p = summary[i]; - novel.summary += loadedCheerio(p).text().trim() + "\\n\\n"; - } - - const totalChapters = loadedCheerio(".eplister li").length; - const chapters: Plugin.ChapterItem[] = loadedCheerio(".eplister li") - .map((chapterIndex, element) => ({ - name: - loadedCheerio(element).find(".epl-num").text() + - loadedCheerio(element).find(".epl-title").text(), - url: loadedCheerio(element).find("a").attr("href") || "", - releaseTime: loadedCheerio(element).find(".epl-date").text().trim(), - chapterNumber: this.options?.reverseChapters - ? totalChapters - chapterIndex - : chapterIndex + 1, - })) - .get() - .filter((chapter) => chapter.name && chapter.url); - - if (this.options?.reverseChapters && chapters.length) chapters.reverse(); - - novel.chapters = chapters; - return novel; - } - - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((res) => res.text()); - const loadedCheerio = load(body); - - const chapterText = loadedCheerio(".epcontent").html() || ""; - return chapterText; - } - - async searchNovels(searchTerm: string): Promise { - const url = this.site + "?s=" + searchTerm; - const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = load(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.maindet").each(function () { - const name = loadedCheerio(this).find("h2").text(); - const image = loadedCheerio(this).find("img"); - const url = loadedCheerio(this).find("h2 a").attr("href"); - - if (!url) return; - novels.push({ - name, - cover: image.attr("data-src") || image.attr("src"), - url, - }); - }); - - return novels; - } - - fetchImage = fetchFile; -} diff --git a/scripts/terser.ts b/scripts/terser.ts new file mode 100644 index 0000000..90fc5f9 --- /dev/null +++ b/scripts/terser.ts @@ -0,0 +1,22 @@ +const { minify_sync } = require("terser"); +import * as fs from "fs"; + +export const minify = function (path: string) { + const code = fs.readFileSync(path).toString(); + const result = minify_sync(code, { + parse: {}, + compress: { + booleans_as_integers: true, + }, + mangle: {}, + ecma: 5, // specify one of: 5, 2015, 2016, etc. + enclose: false, // or specify true, or "args:values" + keep_classnames: false, + keep_fnames: false, + ie8: false, + module: false, + safari10: false, + toplevel: true, + }); + fs.writeFileSync(path, result.code); +}; diff --git a/test_web/api/plugins.ts b/test_web/api/plugins.ts index 3976d09..866abdd 100644 --- a/test_web/api/plugins.ts +++ b/test_web/api/plugins.ts @@ -2,7 +2,7 @@ import fs from "fs"; import { languages } from "@libs/languages"; import path from "path"; import { PluginList } from "@typings/types"; -import { Plugin, isPlugin } from "@typings/plugin"; +import { Plugin } from "@typings/plugin"; import { Filters } from "@libs/filterInputs"; const root = path.dirname(require?.main?.filename || ""); @@ -45,16 +45,9 @@ export const all_plugins = (): PluginList => { const getPlugin = async ( requirePath: string -): Promise => { - console.log("loading plugin", requirePath); +): Promise => { const plugin = await require(requirePath).default; - // console.log(plugin); - if (isPlugin(plugin)) { - console.log("loaded plugin", plugin.name); - return plugin; - } - console.error("Not a plugin!"); - return null; + return plugin; }; export const getFilter = async (pluginRequirePath: string) => @@ -72,15 +65,25 @@ export const searchNovels = async ( searchTerm: string ) => (await getPlugin(pluginRequirePath))?.searchNovels(searchTerm, page); -export const parseNovelAndChapters = async ( +export const parseNovel = async ( pluginRequirePath: string, - novelUrl: string -) => (await getPlugin(pluginRequirePath))?.parseNovelAndChapters(novelUrl); + novelPath: string +) => (await getPlugin(pluginRequirePath))?.parseNovel(novelPath); + +export const hasParsePage = async ( + pluginRequirePath: string, +) => (await getPlugin(pluginRequirePath) as Plugin.PagePlugin | null)?.parsePage; + +export const parsePage = async( + pluginRequirePath: string, + novelPath: string, + page: string +) => (await getPlugin(pluginRequirePath) as Plugin.PagePlugin | null)?.parsePage(novelPath, page); export const parseChapter = async ( pluginRequirePath: string, - chapterUrl: string -) => (await getPlugin(pluginRequirePath))?.parseChapter(chapterUrl); + chapterPath: string +) => (await getPlugin(pluginRequirePath))?.parseChapter(chapterPath); export const fetchImage = async (pluginRequirePath: string, url: string) => (await getPlugin(pluginRequirePath))?.fetchImage(url); diff --git a/test_web/index.html b/test_web/index.html index a14d646..9eb1cfa 100644 --- a/test_web/index.html +++ b/test_web/index.html @@ -155,15 +155,15 @@ - + - parseNovelAndChapters + parseNovel (and first page)

- +
-
@@ -180,13 +180,39 @@ + + + parsePage + + +
+ + +
+ +
+
+
+ Loading... +
+
+
+
+
+
+
+
+
+
+
parseChapter
- +
diff --git a/test_web/static/js/index.js b/test_web/static/js/index.js index e938800..f772d19 100644 --- a/test_web/static/js/index.js +++ b/test_web/static/js/index.js @@ -5,7 +5,8 @@ /** @typedef {import("@typings/plugin").Plugin.PluginItem} PluginItem */ /** @typedef {import("@typings/plugin").Plugin.ChapterItem} ChapterItem */ /** @typedef {import("@typings/plugin").Plugin.NovelItem} NovelItem */ -/** @typedef {import("@typings/plugin").Plugin.SourceNovel} SourceNovel */ +/** @typedef {import("@typings/plugin").Plugin.SourceNovel & {totalPages?: number}} SourceNovel */ +/** @typedef {import("@typings/plugin").Plugin.SourcePage} SourcePage */ /** @typedef {import("@libs/filterInputs").FilterTypes} FilterTypes */ /** @typedef {import("@libs/filterInputs").FilterTypes.CheckboxGroup} CheckboxGroup */ /** @@ -49,8 +50,9 @@ const searchNovels_fetch_btn = $("#searchNovels .fetch-btn"); const searchNovels_searchbar = $("#searchNovels .searchbar input"); /** @type {JQuery} */ -const parseNovelAndChapters = $("#parseNovelAndChapters"); - +const parseNovel = $("#parseNovel"); +/** @type {JQuery} */ +const parsePage = $("#parsePage"); /** @type {JQuery} */ const parseChapter = $("#parseChapter"); @@ -169,7 +171,7 @@ class NovelListPagination { if (p1.length !== p2.length) return false; let i = 0; for (const novel of p1) { - if (novel.url !== p2[i].url) return false; + if (novel.path !== p2[i].path) return false; i++; } return true; @@ -302,8 +304,23 @@ class PluginWrapper { */ constructor(requirePath) { /** @type {string} */ + parsePage.addClass('d-none'); this.requirePath = requirePath; this.currentView = previewSwitch?.[0]?.checked ? "preview" : "raw"; + fetchFromAPI("/hasParsePage/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + pluginRequirePath: this.requirePath, + }), + }).then(res => res.json()) + .then(data => { + if(data.hasParsePage){ + parsePage.removeClass('d-none'); + } + }) } static previewSettings = { @@ -651,7 +668,7 @@ class PluginWrapper { const novel_info = $("
"); novel_info.addClass("novel-info").appendTo(novel_item); this.createInfoItem("name", novel.name).appendTo(novel_info); - this.createInfoItem("url", novel.url).appendTo(novel_info); + this.createInfoItem("path", novel.path).appendTo(novel_info); this.createInfoItem("cover", novel.cover || "undefined").appendTo( novel_info ); @@ -663,9 +680,9 @@ class PluginWrapper { .on("click", () => { // parsable.target.toggle(); /** @type {JQuery} */ - const pnc = $("#parseNovelAndChapters"); + const pnc = $("#parseNovel"); const box = pnc[0]; - $("#parse-novel-url").val(novel.url); + $("#parse-novel-path").val(novel.path); $("#parse-novel-btn").trigger("click"); if (box.collapsed) { box.toggle(); @@ -698,21 +715,21 @@ class PluginWrapper { .appendTo(chapter_item); $("
") .addClass("info-copy btn btn-primary ci-btn1") - .attr("data", chapter.url) - .text("Copy url") + .attr("data", chapter.path) + .text("Copy path") .on("click", () => { - navigator.clipboard.writeText(chapter.url); + navigator.clipboard.writeText(chapter.path); }) .appendTo(chapter_item); $("
") .addClass("info-copy btn btn-primary ci-btn2") - .attr("data", chapter.url) + .attr("data", chapter.path) .text("Parse") .on("click", () => { /** @type {JQuery} */ const parseChapter = $("#parseChapter"); const box = parseChapter[0]; - $("#chapter-parse-url").val(chapter.url); + $("#chapter-parse-path").val(chapter.path); $("#chapter-parse-btn").trigger("click"); if (box.collapsed) { box.toggle(); @@ -804,25 +821,25 @@ class PluginWrapper { } } - async getNovelAndChapters() { + async getNovel() { /** @type {JQuery} */ - const novelUrlInput = $("#parseNovelAndChapters input"); - const novel_item = $("#parseNovelAndChapters .novel-item"); - const chapter_list = $("#parseNovelAndChapters .chapter-list"); - const novelUrl = novelUrlInput.val(); - const spinner = $("#parseNovelAndChapters .spinner-border"); + const novelPathInput = $("#parseNovel input"); + const novel_item = $("#parseNovel .novel-item"); + const chapter_list = $("#parseNovel .chapter-list"); + const novelPath = novelPathInput.val(); + const spinner = $("#parseNovel .spinner-border"); spinner.show(); try { /** @type {SourceNovel | {error:string}} */ const sourceNovel = await ( - await fetchFromAPI(`/parseNovelAndChapters/`, { + await fetchFromAPI(`/parseNovel/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ pluginRequirePath: this.requirePath, - novelUrl: novelUrl, + novelPath, }), }) ).json(); @@ -831,7 +848,7 @@ class PluginWrapper { novel_item.empty(); const novel_data = PluginWrapper.createNovelItem({ name: sourceNovel.name || "undefined", - url: sourceNovel.url, + path: sourceNovel.path, cover: sourceNovel.cover, }); @@ -848,16 +865,20 @@ class PluginWrapper { PluginWrapper.createInfoItem("status", sourceNovel.status), PluginWrapper.createInfoItem("genres", sourceNovel.genres) ); + if(sourceNovel.totalPages){ + novel_data + .children("div + div") + .append( + PluginWrapper.createInfoItem("totalPages", sourceNovel.totalPages.toString()) + ) + } novel_item.replaceWith(novel_data); - console.log(sourceNovel.chapters); - console.log(sourceNovel.chapters?.map((r) => r.url)); - if ( sourceNovel?.chapters?.length !== - new Set(sourceNovel?.chapters?.map((r) => r.url) || []).size + new Set(sourceNovel?.chapters?.map((r) => r.path) || []).size ) { - alert("Chapter urls are the same!"); + alert("Chapter paths are the same!"); } chapter_list.html(""); @@ -872,9 +893,60 @@ class PluginWrapper { spinner.hide(); } } + async getPage () { + /** @type {JQuery} */ + const novelPathInput = $("#parsePage #parse-page-novel-path"); + const pageInput = $("#parsePage #parse-page-index"); + const latestChapter_item = $("#parseNovel latest-chapter-item"); + const chapter_list = $("#parsePage .chapter-list"); + const novelPath = novelPathInput.val(); + const page = pageInput.val(); + const spinner = $("#parsePage .spinner-border"); + spinner.show(); + try { + /** @type {SourcePage | {error:string}} */ + const sourcePage = await ( + await fetchFromAPI(`/parsePage/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + pluginRequirePath: this.requirePath, + novelPath, + page + }), + }) + ).json(); + if ("error" in sourcePage) + throw `Error getting the novel ${sourcePage.error}`; + latestChapter_item.empty(); + if(sourcePage.latestChapter){ + latestChapter_item.append( + PluginWrapper.createChapterItem(sourcePage.latestChapter) + ); + } + if ( + sourcePage?.chapters?.length !== + new Set(sourcePage?.chapters?.map((r) => r.path) || []).size + ) { + alert("Chapter paths are the same!"); + } + chapter_list.html(""); + if (sourcePage.chapters) + for (const chapter of sourcePage.chapters) { + chapter_list.append( + PluginWrapper.createChapterItem(chapter) + ); + } + } catch (/** @type {unknown} */ e) { + } finally { + spinner.hide(); + } + } async getChapter() { - const chapterUrl = $("#parseChapter input").val(); + const chapterPath = $("#parseChapter input").val(); const chapterRawTextarea = $("#parseChapter textarea"); const spinner = $("#parseChapter .spinner-border"); spinner.show(); @@ -887,7 +959,7 @@ class PluginWrapper { }, body: JSON.stringify({ pluginRequirePath: this.requirePath, - chapterUrl: chapterUrl, + chapterPath, }), }) ).text(); @@ -1485,8 +1557,11 @@ $(".searchNovels-btn").on("click", () => state.current_plugin?.getSearchedNovels() ); -$(".parseNovelAndChapters-btn").on("click", () => - state.current_plugin?.getNovelAndChapters() +$(".parseNovel-btn").on("click", () => + state.current_plugin?.getNovel() +); +$(".parsePage-btn").on("click", () => + state.current_plugin?.getPage() ); $(".parseChapter-btn").on("click", () => state.current_plugin?.getChapter()); diff --git a/test_web/static/js/index_old.js b/test_web/static/js/index_old.js deleted file mode 100644 index fe1d6a7..0000000 --- a/test_web/static/js/index_old.js +++ /dev/null @@ -1,1315 +0,0 @@ -// @ts-check -/* Imports */ -/** @typedef {import("@libs/filterInputs").Filters} Filters */ -/** @typedef {import("@typings/types").PluginList} PluginList */ -/** @typedef {import("@typings/plugin").Plugin.PluginItem} PluginItem */ -/** @typedef {import("@typings/plugin").Plugin.ChapterItem} ChapterItem */ -/** @typedef {import("@typings/plugin").Plugin.NovelItem} NovelItem */ -/** @typedef {import("@typings/plugin").Plugin.SourceNovel} SourceNovel */ -/** @typedef {import("@libs/filterInputs").FilterTypes} FilterTypes */ -/** @typedef {import("@libs/filterInputs").FilterTypes.CheckboxGroup} CheckboxGroup */ -/** - * @template {FilterTypes} T - * @typedef {import("@libs/filterInputs").ValueOfFilter} ValueOfFilter */ -/** - * @template {Filters} T - * @typedef {import("@libs/filterInputs").FilterToValues} FilterToValues - */ -/** @typedef {{backgroundColor:string, textColor:string}} Theme */ -/** @typedef {Theme[]} Themes */ -/** @typedef {import("./accordion").AccordionBox} AccordionBox */ - -/// - -const state = { - /** @type {PluginList} */ - all_plugins: {}, - /** @type {Array} */ - plugin_infos: [], - /** @type {{language: string, keyword:null|string}} */ - plugin_search: { - language: "", - keyword: null, - }, - /** @type {Array} */ - plugins_in_search: [], - /** @type {PluginWrapper | undefined} */ - current_plugin: undefined, - /** @type {FilterToValues} */ - filterValues: {}, - /** popularNovels page */ - pop_page: 1, -}; - -/** @type {JQuery}*/ -const language_selection = $("#language"); -/** @type {JQuery} */ -const plugin_search = $("#plugin"); -/** @type {JQuery} */ -const search_results = $("#search-results"); - -const getHeaders = () => { - /** @type {Record} */ - const headers = {}; - const els = document.querySelectorAll(".headerinfo").forEach((el) => { - /** @type {HTMLSpanElement | null} */ - const nameSpan = el.querySelector(".headername"); - /** @type {HTMLSpanElement | null} */ - const valueSpan = el.querySelector(".headervalue"); - if (nameSpan && valueSpan) { - const { innerText: name } = nameSpan; - const { innerText: value } = valueSpan; - if ( - typeof name === "string" && - typeof value === "string" && - value.trim() - ) { - headers[name] = value.trim(); - } - } - }); - return headers; -}; - -/** - * - * @param {string} path - * @param {RequestInit | undefined} init - */ -const fetchFromAPI = async (path, init = undefined) => { - if (init) { - init.headers = { - ...init.headers, - "x-custom-headers": JSON.stringify(getHeaders()), - }; - } - return fetch(path, init); -}; - -const loadPluginsIntoSearchBar = () => { - const { language, keyword } = state.plugin_search; - const plugins = language ? state.all_plugins[language] : state.plugin_infos; - state.plugins_in_search = keyword - ? plugins.filter((p) => - keyword.charAt(0) === "-" - ? !p.name - .toLowerCase() - .includes(keyword.toLowerCase().substring(1)) - : p.name.toLowerCase().includes(keyword.toLowerCase()) - ) - : plugins; -}; - -fetchFromAPI("/all_plugins") - .then((res) => res.json()) - .then((/** @type {PluginList} */ all) => { - state.all_plugins = all; - for (let lang in all) { - language_selection.append(``); - state.plugin_infos = state.plugin_infos.concat(all[lang]); - } - }) - .then((_) => { - loadPluginsIntoSearchBar(); - }); - -language_selection.on("change", (e) => { - state.plugin_search.language = e.target.value; - loadPluginsIntoSearchBar(); -}); - -const createUnclickBox = () => { - // if there is no unclick box - if (!$("#turnoff-catch").length) { - // create it - const turnoff_catch = $("
").attr("id", "turnoff-catch"); - $("body").append(turnoff_catch); - turnoff_catch.on("click", () => { - search_results.hide(); - turnoff_catch.remove(); - }); - } -}; - -const display_search = () => { - search_results.hide(); - search_results.empty(); - createUnclickBox(); - state.plugins_in_search.forEach((plugin) => { - /** @type {JQuery} */ - const img = $("", { src: `./icons/${plugin.icon}` }); - img.css({ - height: "100%", - "object-fit": "contains", - "margin-right": "10px", - }); - /** @type {JQuery} */ - const btn = $("