Beta phase (#801)

* beta host

* Refactor: url -> path (#757)

* refactor: url -> path

* clean up

* fix: json branch

* PagePlugin (#769)

* paginated novel support

* fix: truyenfull

* fix: tunovel, remove pageList

* parsePage support for webtest (#773)

* add parsePage api

* add parsePage UI

* add totalPages

* (FR src) Beta plugin & Added filters (#775)

* noveldeglace:
 - added filters
 - added latest
 - defaultCover fallback
 - change id to noveldeglace.com
 - added complementary infos in summary
 - added cover artist
 - chapters are now parts
 - show volume number for multi volume novels
 - better search
 - fix search page number
 - refactored for plugin beta
 - releaseDate as Date.toString()
 - separted parseNovels
 - separated getCheerio
 - loadedCheerio => $ for shorter lines & simpler test on web

* chireads:
 - fix different parsing for mobile version
 - added filters
 - added author
 - added status
 - added released date
 - refactored for plugin beta
 - separted parseNovels
 - separated getCheerio
 - loadedCheerio => $ for shorter lines & simpler test on web

* wpmangastream => lightnovelwp rework & new sources (#776)

* Added sources:
 - Knoxt (EN)
 - Baca Light Novel (ID)
 - AllNovelRead (ES)
 - DaoTranslate (EN)
 - جنة الروايات (AR)
 - Universal Novel (EN)
 - NovelTR (TR)
old sources auto-generated:
 - GenesisTls (EN)
 - NobleMTL (EN)
wpmangastream => lightnovelwp:
 - added doc
 - added filters in a separte file
 - semi-automatic filter generator
 - made it compatible with version 1.0.X and 1.1.X versions of the theme
 - loadedCheerio => $
 - icons in "/icons/lightnovel/id.png"
 - code cleanup

* removed auto-implemented sources

* Added README.md to lightnovelwp

* removed ai generated text for kolnovel

* added novelsknight

* added centralNovel

* pandamtl accual name

* adding folder plugin/arabic

* Fix: Vietnamese plugins (#777)

* fix: lightnovelvn

* add nettruyen

* add truyenchu

* fix: truyenconect

* Fix: Migrate plugins (#771)

* Add novel.ovh (#756)

* fix

* test

* fix

* date fix + master merge (#761)

* date fix

* Added Brazilian Portuguese E-Novel (#758)

* fixed single digit dates (#767)

Co-authored-by: NaddH1 <naderhorani@hotmail.com>

* fix

* test

* fix order

* Update renovels.ts

* require_chapters

* Delete plugins/english/genesistl.broken.ts

* Update novelTL.ts

---------

Co-authored-by: NaddH1 <naderhorani@hotmail.com>

* (src update) ENG (#772)

* allnovelfull

* bestlightnovel

* divinedaolibrary

* earlynovel

* lightnovelpub

* L

* M

* novelbin

* novelfull

* novelbuddy

* N S

* novel updates

* async sleep

* rename file

* pagePlugin

* Feat: minify js dir (#781)

* Multisrc: HotNovelPub (#784)

* HotNovelPub

* fix search

* Squashed commit of the following:

commit 3b759c68eddc06c0adfb959f775de069a85adca7
Author: nyagami <hoangquan05112002@gmail.com>
Date:   Sat Feb 17 20:33:47 2024 +0700

    Feat: minify js dir (#781)

* jsmin changed to terser

* test

* why I did it.

* test

* Update ranobelib.ts

* fix page

* Update ranobelib.ts

* updated modules

* Update terser.json

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* Update scripts/multisrc/hotnovelpub/template.ts

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* terser api

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* Added sources & fix sources (#787)

* lightnovelwp:
 - fix author name
 - added more localisation of strings
 - README typo
 - error on !r.ok new message

* noveldeglace:
 - fix chapterNumber for parts
 - better dates
 - better search

* Added sources:
 - White Moonlight Novels (EN)
 - System Translation (EN)

* Resized centralnovel.png

* Add Faqwiki.us source (#792)

* Add Faqwiki.us source (beta branch)

* Fix Path, Fix Chapter Parsing, Fix Novel Title

* search novel path fix

* fix chapter parsing. some chapter in this site store their whole text in 1-4 <p>,

* Add WTR-LAB Source (#797)

* Add WTR-LAB Source (Beta Branch)

* Add other language from WTR-LAB source

* change lang to sourceLang

* remove id es tr lang from WTR-LAB

* Update plugins/english/wtrlab.ts

agree

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* Update plugins/english/wtrlab.ts

agree

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* revert: scripts to master

---------

Co-authored-by: Justin COLLON <48163201+error7404@users.noreply.github.com>
Co-authored-by: Rider21 <58046032+Rider21@users.noreply.github.com>
Co-authored-by: NaddH1 <naderhorani@hotmail.com>
Co-authored-by: K1ngfish3r <trulyihu@gmail.com>
Co-authored-by: bregassa <bregas.sat1@gmail.com>
This commit is contained in:
nyagami
2024-02-25 14:52:37 +07:00
committed by GitHub
parent 2b9c8f7d2a
commit d818d1758f
147 changed files with 10278 additions and 5758 deletions
+7 -6
View File
@@ -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<Plugin.SourceNovel> {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
async parseChapter(chapterPath: string): Promise<string> {
// parse chapter text here
const chapterText = "";
return chapterText;
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

+26 -4
View File
@@ -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) {
+201 -57
View File
@@ -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",
+10 -9
View File
@@ -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"
}
View File
-315
View File
@@ -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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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;
}
-170
View File
@@ -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;
}
-168
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
return fetchFile(url);
}
}
export default new NovelFull();
+130 -42
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
return []; /** TO DO */
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
let novel: Plugin.SourceNovel = {
url: novelUrl,
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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());
$('.novel-detail-item').each((i,el) => {
let detailName = $(el).find('h6').text();
let detail = $(el).find('.novel-detail-body');
let $ = parseHTML(result);
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<string> {
const result = await fetchApi(chapterUrl).then((res) => res.text());
let loadedCheerio = parseHTML(result);
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+24 -38
View File
@@ -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<Plugin.SourceNovel> {
const url = novelUrl;
const headers = new Headers();
const result = await fetchApi(url, { headers });
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const headers = new Headers();
const result = await fetchApi(chapterUrl, { headers });
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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);
+201 -102
View File
@@ -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<Plugin.NovelItem[]> {
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");
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,
name: novelName,
path: novelUrl.replace(this.site, ''),
cover: novelCover,
};
@@ -55,126 +37,243 @@ class BLN implements Plugin.PluginBase {
return novels;
}
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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());
console.error(await result.text());
// TODO: Cloudflare protection
// TODO: Cloudflare protection or other error
return [];
}
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
let novel: Plugin.SourceNovel = {
url,
name: "",
cover: "",
author: "",
status: NovelStatus.Unknown,
genres: "",
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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(', ');
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()
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;
}
})
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()
const chapter: Plugin.ChapterItem[] = [];
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)
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();
.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{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;
}
chapter.push({
name: chapterName,
name: chapterName,
releaseTime: releaseDate,
releaseTime: new Date(year, month, day).toISOString(),
path: chapterUrl.replace(this.site, ''),
});
});
novel.chapters = chapter.reverse();
return novel;
}
}
async parseChapter(chapterUrl: string): Promise<string> {
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#vung_doc").html() || "";
return chapterText;
}
async searchNovels(
searchTerm: string,
searchTerm: string,
page: number
): Promise<Plugin.NovelItem[]> {
): Promise<Plugin.NovelItem[]> {
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);
});
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
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();
+21 -31
View File
@@ -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<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const headers = new Headers();
const result = await fetchApi(url, { headers });
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const headers = new Headers();
const result = await fetchApi(chapterUrl, { headers });
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
@@ -132,8 +123,7 @@ class DDLPlugin implements Plugin.PluginBase {
): Promise<Plugin.NovelItem[]> {
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);
+53 -78
View File
@@ -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<typeof this.filters>
@@ -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<Plugin.SourceNovel> {
const url = novelUrl;
const headers = new Headers();
const result = await fetchApi(url, { headers });
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
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<string> {
const headers = new Headers();
const result = await fetchApi(chapterUrl, { headers });
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
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<string> {
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<Plugin.NovelItem[]> {
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);
+40 -41
View File
@@ -14,37 +14,39 @@ class FaqWikiUs implements Plugin.PluginBase {
page: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const body = await fetchApi(url).then((res) => res.text());
async parseNovel(path: string): Promise<Plugin.SourceNovel> {
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<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
const loadedCheerio = parseHTML(body);
loadedCheerio("span").remove();
@@ -86,7 +88,7 @@ class FaqWikiUs implements Plugin.PluginBase {
}).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<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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;
}
-127
View File
@@ -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 });
};
+27 -21
View File
@@ -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<Plugin.SourceNovel> {
const body = await fetchApi(url).then((res) => res.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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;
}
-247
View File
@@ -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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+142 -69
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const body = await fetchApi(url).then((r) => r.text());
let loadedCheerio = parseHTML(body);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
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<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+142 -69
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const body = await fetchApi(url).then((r) => r.text());
let loadedCheerio = parseHTML(body);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
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<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
-180
View File
@@ -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("<br><br>");
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,
};
+77 -55
View File
@@ -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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const result = await fetchApi(url);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
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<Plugin.SourcePage> {
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<string> {
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<string> {
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,
};
+96 -60
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+38 -58
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
return fetchFile(url);
}
-327
View File
@@ -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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+134 -65
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const body = await fetchApi(url).then((r) => r.text());
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+171
View File
@@ -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<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
return fetchFile(url);
}
}
export default new NovelBuddy();
-167
View File
@@ -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;
+107 -75
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const result = await fetchApi(url);
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
const result = await fetchApi(chapterUrl);
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
const searchUrl = `${this.baseUrl}search?keyword=${searchTerm}`;
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+38 -48
View File
@@ -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<Filters>): Promise<Plugin.NovelItem[]> {
const url = `${this.baseUrl}all2022-${pageNo}.html`;
async popularNovels(page: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
let loadedCheerio = parseHTML(body);
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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);
+37 -42
View File
@@ -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<Plugin.SourceNovel> {
const url = novelUrl;
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
async parseChapter(chapterPath: string): Promise<string> {
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, '<br>');
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<Plugin.NovelItem[]> {
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" },
-239
View File
@@ -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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+189
View File
@@ -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<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<string | undefined> {
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();
+23 -28
View File
@@ -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,24 +74,21 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
return this.parseNovels(loadedCheerio);
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const url = novelUrl;
const result = await fetchApi(url);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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())
.toArray()
@@ -104,12 +101,10 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
? '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<string> {
const result = await fetchApi(chapterUrl);
async parseChapter(chapterPath: string): Promise<string> {
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;
}
+286
View File
@@ -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<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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 += `<p>${text}</p>`;
}
return htmlString;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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();
+221 -77
View File
@@ -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<CheerioAPI> {
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<Plugin.NovelItem[]> {
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<Plugin.NovelItem[]> {
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 (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) return;
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");
const novel = { name: novelName, cover: novelCover, url: novelUrl };
if (!novelUrl) return;
novels.push(novel);
});
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<Plugin.SourceNovel> {
const url = novelUrl;
const novel: Plugin.SourceNovel = { url };
const result = await fetchApi(url);
const body = await result.text();
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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<Plugin.NovelItem[]> {
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<string | undefined>{
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();
+289 -135
View File
@@ -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<CheerioAPI | undefined> {
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<Plugin.NovelItem[]> {
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<typeof this.filters>): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const url = novelUrl;
const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
const body = await result.text();
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const $ = await this.getCheerio(this.site + novelPath);
if (!$)
throw new Error("Failed to load page (open in web view)");
let loadedCheerio = load(body);
let novel: Plugin.SourceNovel = { path: novelPath, name: "Untitled" };
let novel: Plugin.SourceNovel = { url };
novel.name = ($("div.entry-content > div > strong")[0]
.nextSibling as Text | null)
?.nodeValue
?.trim() || "Untitled";
novel.name = (loadedCheerio("div.entry-content > div > strong"
)[0].nextSibling as Text | null)?.nodeValue?.trim() || '';
novel.cover = $(".su-row > div > div > img")
.attr("src") || defaultCover;
novel.cover = loadedCheerio(".su-row > div > div > img").attr("src");
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.summary = loadedCheerio("div[data-title=Synopsis]").text();
novel.author = $("strong:contains('Auteur :')")
.parent()
.text()
.replace("Auteur : ", "")
.trim();
novel.author = loadedCheerio("strong:contains('Auteur :')"
).parent().text().replace("Auteur : ", "").trim();
novel.artist = $("strong:contains('Illustrateur :')")
.parent()
.text()
.replace("Illustrateur :", "")
.trim();
// novel.artist = loadedCheerio("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;
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 = $("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 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[] = [];
let novelChapters: Plugin.ChapterItem[] = [];
const volumes = $("div[data-title=Tomes] > div")
.last()
.contents();
const hasMultipleVolumes = volumes.length > 1;
loadedCheerio(".chpt").each(function () {
const chapterName = loadedCheerio(this).find("a").text().trim();
const releaseDate = null;
const chapterUrl = loadedCheerio(this).find("a").attr("href");
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("</a>") || 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,
};
if (!chapterUrl) return;
novelChapters.push(chapter);
}
}
else // has parts that needs to be added individually
{
const items = cheerio
.find("i")
.parent()
.next()
.html()
?.split("</a>") || [];
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);
}
);
}
});
});
const chapter = {
name: chapterName,
releaseTime: releaseDate,
url: chapterUrl,
};
novel.chapters = novelChapters;
novelChapters.push(chapter);
});
return novel;
};
novel.chapters = novelChapters;
async parseChapter(chapterPath: string): Promise<string> {
const $ = await this.getCheerio(this.site + chapterPath);
if (!$)
throw new Error("Failed to load page (open in web view)");
return novel;
};
async parseChapter(chapterUrl: string): Promise<string> {
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<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string, num: number): Promise<Plugin.NovelItem[]> {
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();
const $ = await this.getCheerio(url);
if (!$)
throw new Error("Failed to load page (open in web view)");
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);
});
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<string | undefined>{
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();
+25 -19
View File
@@ -14,7 +14,10 @@ class freedlit implements Plugin.PluginBase {
async popularNovels(
page: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((res) => res.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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(),
@@ -79,9 +85,9 @@ class freedlit implements Plugin.PluginBase {
const releaseDate = loadedCheerio(element).find('span[class="date"]').text();
chapters.push({
name,
url,
path: url.replace(this.site, ""),
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1
chapterNumber: chapterIndex + 1,
});
});
@@ -89,8 +95,8 @@ class freedlit implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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;
+24 -18
View File
@@ -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<Plugin.SourceNovel> {
const workID = novelUrl.split("/")[4];
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const result = await fetchApi(chapterUrl, {
async parseChapter(chapterPath: string): Promise<string> {
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" },
+21 -13
View File
@@ -14,10 +14,15 @@ class Bookriver implements Plugin.PluginBase {
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const result = await fetchApi(novelUrl).then((res) => res.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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 = {
+15 -17
View File
@@ -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<Plugin.SourceNovel> {
const result = await fetchApi(novelUrl).then((res) => res.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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();
@@ -101,20 +100,19 @@ class ficbook implements Plugin.PluginBase {
const releaseTime = loadedCheerio(element).find("div > span").attr("title");
chapters.push({
name,
url: this.site + url,
path: url.replace(this.site, ""),
releaseTime,
chapterNumber: chapterIndex + 1,
});
});
}
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const result = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterPath: string): Promise<string> {
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;
+28 -25
View File
@@ -14,7 +14,10 @@ class Jaomix implements Plugin.PluginBase {
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((res) => res.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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(),
@@ -102,9 +104,9 @@ class Jaomix implements Plugin.PluginBase {
const releaseDate = loadedCheerio(element).find("time").text();
chapters.push({
name,
url,
path: url.replace(this.site, ""),
releaseTime: this.parseDate(releaseDate),
chapterNumber: totalChapters - chapterIndex
chapterNumber: totalChapters - chapterIndex,
});
});
@@ -112,8 +114,8 @@ class Jaomix implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
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;
}
+11 -11
View File
@@ -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<Plugin.SourceNovel> {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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(","),
@@ -68,9 +68,9 @@ class novelOvh implements Plugin.PluginBase {
name:
chapter.title ||
"Том " + (chapter.volume || 0) + " " +
(chapter.name || "Глава " +
(chapter.number || json.chapters.length - chapterIndex)),
url: novelUrl + "/" + chapter.id + "/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<string> {
async parseChapter(chapterPath: string): Promise<string> {
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,
}),
);
+22 -19
View File
@@ -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<Plugin.SourceNovel> {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
async parseChapter(chapterPath: string): Promise<string> {
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,
+41 -35
View File
@@ -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,7 +20,10 @@ class RNBH implements Plugin.PluginBase {
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/api/search?page=" + pageNo + "&sort=";
url += showLatestNovels
@@ -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<Plugin.SourceNovel> {
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<Plugin.SourceNovel> {
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<string> {
const result = await fetchApi(chapterUrl);
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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"),
}),
);
+96 -61
View File
@@ -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,11 +22,15 @@ class RLIB implements Plugin.PluginBase {
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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) {
@@ -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<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((res) => res.text());
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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;
+13 -13
View File
@@ -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<Plugin.SourceNovel> {
const result = await fetchApi(novelUrl);
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const result = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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 = {
+29 -32
View File
@@ -4,29 +4,31 @@ 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<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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 + "=" +
@@ -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<Plugin.SourceNovel> {
const novelID = novelUrl.split("/")[4];
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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[];
@@ -98,7 +104,7 @@ class ReN implements Plugin.PluginBase {
"Том " + 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<string> {
const url = this.site + "/api/titles/chapters/" + chapterUrl.split("/")[5];
async parseChapter(chapterPath: string): Promise<string> {
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: [] },
+66 -91
View File
@@ -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<Plugin.SourceNovel> {
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<Plugin.SourceNovel & { totalPages: number; }> {
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<string> {
const result = await fetchApi(chapterUrl);
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
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<string> {
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;
+21 -27
View File
@@ -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<Plugin.SourceNovel> {
const id = novelUrl.split("/").pop();
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
const id = chapterUrl.split("/").pop();
async parseChapter(chapterPath: string): Promise<string> {
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<Plugin.NovelItem[]> {
const result = await fetchApi(
"https://api.smakolykytl.site/api/user/projects",
);
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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;

Some files were not shown because too many files have changed in this diff Show More