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 0000000..ce8cc6d Binary files /dev/null and b/icons/multisrc/hotnovelpub/icons/eznovels.png differ diff --git a/icons/multisrc/hotnovelpub/icons/hotnovelpub.png b/icons/multisrc/hotnovelpub/icons/hotnovelpub.png new file mode 100644 index 0000000..0461f8a Binary files /dev/null and b/icons/multisrc/hotnovelpub/icons/hotnovelpub.png differ diff --git a/icons/multisrc/hotnovelpub/icons/lanovels.png b/icons/multisrc/hotnovelpub/icons/lanovels.png new file mode 100644 index 0000000..a18ad41 Binary files /dev/null and b/icons/multisrc/hotnovelpub/icons/lanovels.png differ diff --git a/icons/multisrc/hotnovelpub/icons/lightnoveldaily.png b/icons/multisrc/hotnovelpub/icons/lightnoveldaily.png new file mode 100644 index 0000000..e2dbbc8 Binary files /dev/null and b/icons/multisrc/hotnovelpub/icons/lightnoveldaily.png differ diff --git a/icons/multisrc/hotnovelpub/icons/thnovels.png b/icons/multisrc/hotnovelpub/icons/thnovels.png new file mode 100644 index 0000000..a18ad41 Binary files /dev/null and b/icons/multisrc/hotnovelpub/icons/thnovels.png differ diff --git a/icons/multisrc/lightnovelwp/allnovelread.png b/icons/multisrc/lightnovelwp/allnovelread.png new file mode 100644 index 0000000..0e99931 Binary files /dev/null and b/icons/multisrc/lightnovelwp/allnovelread.png differ diff --git a/icons/multisrc/lightnovelwp/bacalightnovel.png b/icons/multisrc/lightnovelwp/bacalightnovel.png new file mode 100644 index 0000000..e1ee2c1 Binary files /dev/null and b/icons/multisrc/lightnovelwp/bacalightnovel.png differ diff --git a/icons/multisrc/lightnovelwp/centralnovel.png b/icons/multisrc/lightnovelwp/centralnovel.png new file mode 100644 index 0000000..443ae02 Binary files /dev/null and b/icons/multisrc/lightnovelwp/centralnovel.png differ diff --git a/icons/multisrc/lightnovelwp/daotranslate.png b/icons/multisrc/lightnovelwp/daotranslate.png new file mode 100644 index 0000000..989b25f Binary files /dev/null and b/icons/multisrc/lightnovelwp/daotranslate.png differ diff --git a/icons/multisrc/lightnovelwp/genesistls.png b/icons/multisrc/lightnovelwp/genesistls.png new file mode 100644 index 0000000..f9892a4 Binary files /dev/null and b/icons/multisrc/lightnovelwp/genesistls.png differ diff --git a/icons/multisrc/lightnovelwp/knoxt.png b/icons/multisrc/lightnovelwp/knoxt.png new file mode 100644 index 0000000..a419e82 Binary files /dev/null and b/icons/multisrc/lightnovelwp/knoxt.png differ diff --git a/icons/multisrc/lightnovelwp/kolnovel.png b/icons/multisrc/lightnovelwp/kolnovel.png new file mode 100644 index 0000000..cc586d0 Binary files /dev/null and b/icons/multisrc/lightnovelwp/kolnovel.png differ diff --git a/icons/multisrc/lightnovelwp/lightnovelbrasil.png b/icons/multisrc/lightnovelwp/lightnovelbrasil.png new file mode 100644 index 0000000..4cf8955 Binary files /dev/null and b/icons/multisrc/lightnovelwp/lightnovelbrasil.png differ diff --git a/icons/multisrc/lightnovelwp/lightnovelfr.png b/icons/multisrc/lightnovelwp/lightnovelfr.png new file mode 100644 index 0000000..045111f Binary files /dev/null and b/icons/multisrc/lightnovelwp/lightnovelfr.png differ diff --git a/icons/multisrc/lightnovelwp/noblemtl.png b/icons/multisrc/lightnovelwp/noblemtl.png new file mode 100644 index 0000000..2b38852 Binary files /dev/null and b/icons/multisrc/lightnovelwp/noblemtl.png differ diff --git a/icons/multisrc/lightnovelwp/novelsknight.png b/icons/multisrc/lightnovelwp/novelsknight.png new file mode 100644 index 0000000..5d237f1 Binary files /dev/null and b/icons/multisrc/lightnovelwp/novelsknight.png differ diff --git a/icons/multisrc/lightnovelwp/novelsparadise.png b/icons/multisrc/lightnovelwp/novelsparadise.png new file mode 100644 index 0000000..55deeb8 Binary files /dev/null and b/icons/multisrc/lightnovelwp/novelsparadise.png differ diff --git a/icons/multisrc/lightnovelwp/noveltr.png b/icons/multisrc/lightnovelwp/noveltr.png new file mode 100644 index 0000000..25cd930 Binary files /dev/null and b/icons/multisrc/lightnovelwp/noveltr.png differ diff --git a/icons/multisrc/lightnovelwp/pandamtl.png b/icons/multisrc/lightnovelwp/pandamtl.png new file mode 100644 index 0000000..5a81017 Binary files /dev/null and b/icons/multisrc/lightnovelwp/pandamtl.png differ diff --git a/icons/multisrc/lightnovelwp/systemtranslation.png b/icons/multisrc/lightnovelwp/systemtranslation.png new file mode 100644 index 0000000..fb15d47 Binary files /dev/null and b/icons/multisrc/lightnovelwp/systemtranslation.png differ diff --git a/icons/multisrc/lightnovelwp/universalnovel.png b/icons/multisrc/lightnovelwp/universalnovel.png new file mode 100644 index 0000000..ef9c1db Binary files /dev/null and b/icons/multisrc/lightnovelwp/universalnovel.png differ diff --git a/icons/multisrc/lightnovelwp/whitemoonlightnovels.png b/icons/multisrc/lightnovelwp/whitemoonlightnovels.png new file mode 100644 index 0000000..ee12995 Binary files /dev/null and b/icons/multisrc/lightnovelwp/whitemoonlightnovels.png differ diff --git a/icons/src/en/genesistls/icon.png b/icons/src/en/genesistls/icon.png deleted file mode 100644 index 56068c5..0000000 Binary files a/icons/src/en/genesistls/icon.png and /dev/null differ diff --git a/icons/src/en/lightnovelreader/icon.png b/icons/src/en/lightnovelreader/icon.png deleted file mode 100644 index f60654b..0000000 Binary files a/icons/src/en/lightnovelreader/icon.png and /dev/null differ diff --git a/icons/src/en/noblemtl/icon.png b/icons/src/en/noblemtl/icon.png deleted file mode 100644 index 39423fb..0000000 Binary files a/icons/src/en/noblemtl/icon.png and /dev/null differ diff --git a/icons/src/en/novelbuddy/icon.png b/icons/src/en/novelbuddy/icon.png new file mode 100644 index 0000000..f0b314f Binary files /dev/null and b/icons/src/en/novelbuddy/icon.png differ diff --git a/icons/src/en/novelfullme/icon.png b/icons/src/en/novelfullme/icon.png deleted file mode 100644 index 952e1ae..0000000 Binary files a/icons/src/en/novelfullme/icon.png and /dev/null differ diff --git a/icons/src/en/novelsonline/icon.png b/icons/src/en/novelsonline/icon.png new file mode 100644 index 0000000..19a2ccf Binary files /dev/null and b/icons/src/en/novelsonline/icon.png differ diff --git a/icons/src/en/readlitenovel/icon.png b/icons/src/en/readlitenovel/icon.png new file mode 100644 index 0000000..165a2e5 Binary files /dev/null and b/icons/src/en/readlitenovel/icon.png differ diff --git a/icons/src/en/wtrlab/icon.png b/icons/src/en/wtrlab/icon.png new file mode 100644 index 0000000..169abdb Binary files /dev/null and b/icons/src/en/wtrlab/icon.png differ diff --git a/icons/src/vi/nettruyen/icon.png b/icons/src/vi/nettruyen/icon.png new file mode 100644 index 0000000..468ed5b Binary files /dev/null and b/icons/src/vi/nettruyen/icon.png differ diff --git a/icons/src/vi/truyenchu/icon.png b/icons/src/vi/truyenchu/icon.png new file mode 100644 index 0000000..bce6cd5 Binary files /dev/null and b/icons/src/vi/truyenchu/icon.png differ 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 = $("