diff --git a/icons/src/ru/neobook/icon.png b/icons/src/ru/neobook/icon.png new file mode 100644 index 0000000..b81a7b6 Binary files /dev/null and b/icons/src/ru/neobook/icon.png differ diff --git a/index.ts b/index.ts index 0e49ffd..6133cef 100644 --- a/index.ts +++ b/index.ts @@ -143,6 +143,18 @@ app.post("/fetchImage/", async (req, res) => { res.json({ error: String(err) }); } }); +app.post("/resolveUrl/", async (req, res) => { + try { + const fullURL = await pluginApi.resolveUrl( + req.body["pluginRequirePath"], + req.body["path"], + req.body["isNovel"], + ); + res.send(fullURL); + } catch (err: unknown) { + res.json({ error: String(err) }); + } +}); app.get("/", (req, res) => { res.sendFile(path.join(dirname, "test_web", "index.html")); diff --git a/libs/fetch.ts b/libs/fetch.ts index 2fbbd10..55480a4 100644 --- a/libs/fetch.ts +++ b/libs/fetch.ts @@ -57,7 +57,7 @@ const makeInit = async (init?: FetchInit) => { export async function fetchApi(url: string, init?: FetchInit) { init = await makeInit(init); // console.log(init.headers); - // console.log(url, init); + console.log(url, init); return await fetch(url, init as RequestInit); } diff --git a/package-lock.json b/package-lock.json index 1dd3b3c..32db427 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,9 @@ "express": "^4.18.2", "less": "^4.2.0", "module-alias": "^2.2.3", - "sanitize-html": "^2.11.0", - "terser": "^5.27.2", + "qs": "^6.11.2", + "sanitize-html": "^2.12.1", + "terser": "^5.28.1", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", "typescript": "^5.3.3", @@ -463,6 +464,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1478,6 +1493,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -2861,9 +2890,9 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, @@ -3064,9 +3093,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-html": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", - "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", + "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -3436,9 +3465,9 @@ } }, "node_modules/terser": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.2.tgz", - "integrity": "sha512-sHXmLSkImesJ4p5apTeT63DsV4Obe1s37qT8qvwHRmVxKTBH7Rv9Wr26VcAMmLbmk9UliiwK8z+657NyJHHy/w==", + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz", + "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -3728,20 +3757,6 @@ "qs": "^6.11.2" } }, - "node_modules/url/node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/urlencode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/urlencode/-/urlencode-2.0.0.tgz", @@ -4224,6 +4239,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -5079,6 +5102,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, "raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -6126,9 +6157,9 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "requires": { "side-channel": "^1.0.4" } @@ -6261,9 +6292,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-html": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", - "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", + "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", "requires": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -6570,9 +6601,9 @@ } }, "terser": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.2.tgz", - "integrity": "sha512-sHXmLSkImesJ4p5apTeT63DsV4Obe1s37qT8qvwHRmVxKTBH7Rv9Wr26VcAMmLbmk9UliiwK8z+657NyJHHy/w==", + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz", + "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==", "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -6781,16 +6812,6 @@ "requires": { "punycode": "^1.4.1", "qs": "^6.11.2" - }, - "dependencies": { - "qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "requires": { - "side-channel": "^1.0.4" - } - } } }, "urlencode": { diff --git a/package.json b/package.json index 11e7176..d16a4a4 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,9 @@ "express": "^4.18.2", "less": "^4.2.0", "module-alias": "^2.2.3", - "sanitize-html": "^2.11.0", - "terser": "^5.27.2", + "qs": "^6.11.2", + "sanitize-html": "^2.12.1", + "terser": "^5.28.1", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", "typescript": "^5.3.3", diff --git a/plugins/english/NovelOnline.ts b/plugins/english/NovelOnline.ts index d113e65..fbdf410 100644 --- a/plugins/english/NovelOnline.ts +++ b/plugins/english/NovelOnline.ts @@ -2,6 +2,7 @@ import { load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; import { Filters, FilterTypes } from "@libs/filterInputs"; +import qs from "qs"; class NovelsOnline implements Plugin.PluginBase { id = "NO.net"; @@ -115,7 +116,7 @@ class NovelsOnline implements Plugin.PluginBase { "application/x-www-form-urlencoded; charset=UTF-8", }, method: "POST", - body: "q=" + encodeURIComponent(searchTerm), + body: qs.stringify({ q: searchTerm }), }, ).then((res) => res.text()); diff --git a/plugins/english/freewebnovel.ts b/plugins/english/freewebnovel.ts index 6d0673f..112a5fc 100644 --- a/plugins/english/freewebnovel.ts +++ b/plugins/english/freewebnovel.ts @@ -2,6 +2,7 @@ import { Plugin } from "@typings/plugin"; import { fetchApi, fetchFile } from "@libs/fetch"; import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; +import qs from "qs"; class FreeWebNovel implements Plugin.PluginBase { id = "FWN.com"; @@ -92,7 +93,7 @@ class FreeWebNovel implements Plugin.PluginBase { Origin: this.site, }, method: "POST", - body: "searchkey=" + encodeURIComponent(searchTerm), + body: qs.stringify({ searchkey: searchTerm }), }).then((res) => res.text()); const loadedCheerio = parseHTML(body); diff --git a/plugins/korean/Agitoon.broken.ts b/plugins/korean/Agitoon.broken.ts deleted file mode 100644 index bb94a1b..0000000 --- a/plugins/korean/Agitoon.broken.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; - -export const id = "agit.xyz"; -export const name = "Agitoon"; -export const site = "https://agit501.xyz/"; -export const version = "1.0.0"; -export const icon = "src/kr/agitoon/agit.png"; - -const baseUrl = site; - -class Agitoon implements Plugin.PluginBase { - id = "agit.xyz"; - name = "Agitoon"; - icon = "src/kr/agitoon/agit.png"; - site = "https://agit501.xyz/"; - filters?: Filters | undefined; - version = "1.0.0"; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const list_limit = 20 * (pageNo - 1); - const day = new Date().getDay(); - - const res = await fetchApi(baseUrl + "novel/index.update.php", { - headers: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", - }, - body: `mode=get_data_novel_list_p&novel_menu=3&np_day=${day}&np_rank=1&np_distributor=0&np_genre=00&np_order=1&np_genre_ex_1=00&np_genre_ex_2=00&list_limit=${list_limit}&is_query_first=true`, - method: "POST", - }); - const resJson = await res.json() as response; - - const novels: Plugin.NovelItem[] = [] - - resJson?.list?.forEach((novel) => { - novels.push({ - url: baseUrl + "novel/list/" + novel.wr_id, - name: novel.wr_subject, - cover: baseUrl + novel.np_dir + "/thumbnail/" + novel.np_thumbnail, - }); - }); - - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const novelId = novelUrl.split("/").reverse()[0]; - - // cheerio - const result = await fetchApi(novelUrl); - const body = await result.text(); - - const loadedCheerio = parseHTML(body, { decodeEntities: false }); - const name = loadedCheerio("h5.pt-2").text(); - const summary = loadedCheerio(".pt-1.mt-1.pb-1.mb-1").text(); - const author = loadedCheerio(".post-item-list-cate-v") - .first() - .text() - .split(" : ") - .reverse()[0]; - const cover = - baseUrl.slice(0, baseUrl.length - 1) + - loadedCheerio("div.col-5.pr-0.pl-0 img").attr("src"); - const genresTag = loadedCheerio(".col-7 > .post-item-list-cate > span"); - let genres = ""; - - genresTag.each((_, element) => { - genres += loadedCheerio(element).text(); - genres += ", "; - }); - genres = genres.slice(0, genres.length - 2); - - // normal REST HTTP requests - let chapters: Plugin.ChapterItem[] = []; - - const res = await fetchApi(baseUrl + "novel/list.update.php", { - headers: { - "content-type": - "application/x-www-form-urlencoded; charset=UTF-8", - }, - body: `mode=get_data_novel_list_c&wr_id_p=${novelId}&page_no=1&cnt_list=10000&order_type=Asc`, - method: "POST", - }); - - const resJson = await res.json() as responseBook - - resJson?.list?.forEach((chapter) => { - chapters.push({ - name: chapter.wr_subject, - url: baseUrl + `novel/view/${chapter.wr_id}/2`, - releaseTime: chapter.wr_datetime, - }); - }); - - const novel: Plugin.SourceNovel = { - url: novelUrl, - name, - cover, - summary, - author, - status: "", - genres: genres, - chapters, - }; - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const result = await fetchApi(chapterUrl); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let content = loadedCheerio("#id_wr_content").html() || ''; - - // gets rid of the popup thingy - content = content.replace( - "팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다", - "" - ); - - return content; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const rawResults = await fetchApi("https://agit501.xyz/novel/search.php", { - headers: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", - }, - body: `mode=get_data_novel_list_p_sch&search_novel=${searchTerm}&list_limit=0`, - method: "POST", - }); - const resJson = await rawResults.json() as response; - - const novels: Plugin.NovelItem[] = [] - - resJson?.list?.forEach((novel) => { - novels.push({ - url: baseUrl + "novel/list/" + novel.wr_id, - name: novel.wr_subject, - cover: baseUrl + novel.np_dir + "/thumbnail/" + novel.np_thumbnail, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - -} - -export default new Agitoon(); - -interface response { - list_limit: number; - list?: (ListEntity)[] | null; - list_count: number; -} -interface ListEntity { - wr_id: string; - wr_subject: string; - np_dir: string; - np_type_02: string; - np_thumbnail: string; - np_author: string; - wr_subject2: string; - wr_datetime: string; - np_distributor: string; - np_genre: string; - np_country: string; - np_age: string; - is_scrap: number; -} - -interface responseBook { - list?: (ListEntity2)[] | null; - download_time: string; -} -interface ListEntity2 { - wr_id: string; - wr_subject: string; - wr_datetime: string; - url_view1: string; - novel_c_style1: string; - novel_c_str1: string; - data_novel_c_view: string; -} diff --git a/plugins/korean/Agitoon.ts b/plugins/korean/Agitoon.ts new file mode 100644 index 0000000..6ec908f --- /dev/null +++ b/plugins/korean/Agitoon.ts @@ -0,0 +1,185 @@ +import { fetchFile, fetchApi } from "@libs/fetch"; +import { Filters } from "@libs/filterInputs"; +import { Plugin } from "@typings/plugin"; +import { load as parseHTML } from "cheerio"; +import qs from "qs"; + +class Agitoon implements Plugin.PluginBase { + id = "agit.xyz"; + name = "Agitoon"; + icon = "src/kr/agitoon/agit.png"; + site = "https://agit501.xyz"; + version = "1.0.0"; + + async popularNovels( + pageNo: number, + { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + const res = await fetchApi(this.site + "/novel/index.update.php", { + headers: { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + }, + method: "POST", + body: qs.stringify({ + mode: "get_data_novel_list_p", + novel_menu: showLatestNovels ? "1" : "3", + np_day: new Date().getDay(), + np_rank: "1", + np_distributor: "0", + np_genre: "00", + np_order: "1", + np_genre_ex_1: "00", + np_genre_ex_2: "00", + list_limit: 20 * (pageNo - 1), + is_query_first: pageNo == 1, + }), + }); + const resJson = (await res.json()) as response; + const novels: Plugin.NovelItem[] = []; + + resJson?.list?.forEach((novel) => + novels.push({ + name: novel.wr_subject, + cover: this.site + novel.np_dir + "/thumbnail/" + novel.np_thumbnail, + path: novel.wr_id, + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.resolveUrl(novelPath, true)) + .then((res) => res.text()); + const loadedCheerio = parseHTML(result, { decodeEntities: false }); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio("h5.pt-2").text(), + cover: this.site + loadedCheerio("div.col-5.pr-0.pl-0 img").attr("src"), + summary: loadedCheerio(".pt-1.mt-1.pb-1.mb-1").text(), + }; + + novel.author = loadedCheerio(".post-item-list-cate-v") + .first() + .text() + .split(" : ")[1]; + + const genres = loadedCheerio(".col-7 > .post-item-list-cate > span") + .map((index, element) => loadedCheerio(element).text().trim()) + .get(); + + if (genres.length) { + novel.genres = genres.join(", "); + } + + const chapters: Plugin.ChapterItem[] = []; + const res = await fetchApi(this.site + "/novel/list.update.php", { + headers: { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + }, + method: "POST", + body: qs.stringify({ + mode: "get_data_novel_list_c", + wr_id_p: novelPath, + page_no: "1", + cnt_list: "10000", + order_type: "Asc", + }), + }); + + const resJson = (await res.json()) as responseBook; + resJson?.list?.forEach((chapter) => + chapters.push({ + name: chapter.wr_subject, + path: chapter.wr_id + "/2", + releaseTime: chapter.wr_datetime, + }), + ); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.resolveUrl(chapterPath)) + .then((res) => res.text()); + + const loadedCheerio = parseHTML(result); + let content = loadedCheerio("#id_wr_content").html() || ""; + + // gets rid of the popup thingy + content = content + .replace("팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다", "") + .trim(); + + return content; + } + + async searchNovels(searchTerm: string): Promise { + const rawResults = await fetchApi("https://agit501.xyz/novel/search.php", { + headers: { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + }, + method: "POST", + body: qs.stringify({ + mode: "get_data_novel_list_p_sch", + search_novel: searchTerm, + list_limit: 0, + }), + }); + const resJson = (await rawResults.json()) as response; + const novels: Plugin.NovelItem[] = []; + + resJson?.list?.forEach((novel) => + novels.push({ + name: novel.wr_subject, + cover: this.site + "/" + novel.np_dir + "/thumbnail/" + novel.np_thumbnail, + path: novel.wr_id, + }), + ); + + return novels; + } + + fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? "/novel/list/" : "/novel/view/") + path; +} + +export default new Agitoon(); + +interface response { + list_limit: number; + list?: ListEntity[] | null; + list_count: number; +} +interface ListEntity { + wr_id: string; + wr_subject: string; + np_dir: string; + np_type_02: string; + np_thumbnail: string; + np_author: string; + wr_subject2: string; + wr_datetime: string; + np_distributor: string; + np_genre: string; + np_country: string; + np_age: string; + is_scrap: number; +} + +interface responseBook { + list?: ListEntity2[] | null; + download_time: string; +} +interface ListEntity2 { + wr_id: string; + wr_subject: string; + wr_datetime: string; + url_view1: string; + novel_c_style1: string; + novel_c_str1: string; + data_novel_c_view: string; +} diff --git a/plugins/russian/LitSpace.ts b/plugins/russian/LitSpace.ts index 2449805..32209a3 100644 --- a/plugins/russian/LitSpace.ts +++ b/plugins/russian/LitSpace.ts @@ -112,7 +112,7 @@ class freedlit implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio("#bookListBlock > div").each((index, element) => { + loadedCheerio("#bookGridBlock > 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(); diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index 294f37d..cf50a3a 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -53,15 +53,14 @@ class AuthorToday implements Plugin.PluginBase { cover: novel.coverUrl ? "https://cm.author.today/content/" + novel.coverUrl : defaultCover, - path: "/work/" + novel.id, + path: novel.id.toString(), }), ); return novels; } - async parseNovel(novelPath: string): Promise { - const workID = novelPath.split("/")[2]; + async parseNovel(workID: string): Promise { const result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, { headers: { Authorization: token, @@ -70,7 +69,7 @@ class AuthorToday implements Plugin.PluginBase { const book = (await result.json()) as responseBook; const novel: Plugin.SourceNovel = { - path: novelPath, + path: workID, name: book.title, cover: book.coverUrl ? book.coverUrl.split("?")[0] : defaultCover, genres: book.tags?.join(", "), @@ -105,7 +104,7 @@ class AuthorToday implements Plugin.PluginBase { if (chapter.isAvailable && !chapter.isDraft) { chapters.push({ name: chapter.title || "Глава " + (chapterIndex + 1), - path: `v1/work/${workID}/chapter/${chapter.id}/text`, + path: workID + "/" + chapter.id, releaseTime: dayjs( chapter.publishTime || chapter.lastModificationTime, ).format("LLL"), @@ -119,11 +118,15 @@ class AuthorToday implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(apiUrl + chapterPath, { - headers: { - Authorization: token, + const [workID, chapterID] = chapterPath.split("/"); + const result = await fetchApi( + apiUrl + `v1/work/${workID}/chapter/${chapterID}/text`, + { + headers: { + Authorization: token, + }, }, - }); + ); const json = (await result.json()) as encryptedСhapter; if (json.code) { @@ -160,26 +163,26 @@ class AuthorToday implements Plugin.PluginBase { const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = []; - loadedCheerio("div.book-row").each((index, element) => { + loadedCheerio("a.work-row").each((index, element) => { const name = loadedCheerio(element) - .find('div[class="book-title"] a') + .find('h4[class="work-title"]') .text() .trim(); - let cover = loadedCheerio(element).find("img").attr("src"); - const path = loadedCheerio(element) - .find('div[class="book-title"] a') - .attr("href"); + let cover = loadedCheerio(element).find("img").attr("data-src"); + const path = loadedCheerio(element).attr("href"); cover = cover ? cover.split("?")[0] : defaultCover; if (!path) return; - novels.push({ name, cover, path }); + novels.push({ name, cover, path: path.replace("/work/", "") }); }); return novels; } fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => + isNovel ? this.site + "/work/" + path : this.site + "/reader/" + path; filters = { sort: { diff --git a/plugins/russian/bookriver.ts b/plugins/russian/bookriver.ts index dcc8823..7864a66 100644 --- a/plugins/russian/bookriver.ts +++ b/plugins/russian/bookriver.ts @@ -39,7 +39,7 @@ class Bookriver implements Plugin.PluginBase { novels.push({ name: novel.name, cover: novel.coverImages[0].url, - path: "/book/" + novel.slug, + path: novel.slug, }), ); } @@ -47,7 +47,8 @@ class Bookriver implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath).then((res) => res.text()); + const result = await fetchApi(this.resolveUrl(novelPath, true)) + .then((res) => res.text()); const loadedCheerio = parseHTML(result); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); @@ -72,7 +73,7 @@ class Bookriver implements Plugin.PluginBase { if (chapter.available) { chapters.push({ name: chapter.name, - path: "/reader/" + book?.slug + "/" + chapter.chapterId, + path: book?.slug + "/" + chapter.chapterId, releaseTime: dayjs( chapter.firstPublishedAt || chapter.createdAt || undefined, ).format("LLL"), @@ -112,7 +113,7 @@ class Bookriver implements Plugin.PluginBase { novels.push({ name: novel.name, cover: novel.coverImages[0].url, - path: "/book/" + novel.slug, + path: novel.slug, }), ); @@ -120,6 +121,8 @@ class Bookriver implements Plugin.PluginBase { } fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? "/book/" : "/reader/") + path; filters = { sort: { diff --git a/plugins/russian/neobook.ts b/plugins/russian/neobook.ts new file mode 100644 index 0000000..ea7556e --- /dev/null +++ b/plugins/russian/neobook.ts @@ -0,0 +1,450 @@ +import { Plugin } from "@typings/plugin"; +import { FilterTypes, Filters } from "@libs/filterInputs"; +import { defaultCover } from "@libs/defaultCover"; +import { fetchApi, fetchFile } from "@libs/fetch"; +import { NovelStatus } from "@libs/novelStatus"; + +const statusKey: { [key: string]: string } = { + "0": NovelStatus.Unknown, + "1": NovelStatus.Ongoing, + "2": NovelStatus.Completed, + "3": NovelStatus.OnHiatus, + "4": NovelStatus.Cancelled, +}; + +class Neobook implements Plugin.PluginBase { + id = "neobook"; + name = "Neobook"; + site = "https://neobook.org"; + version = "1.0.0"; + icon = "src/ru/neobook/icon.png"; + + async fetchNovels( + page: number, + { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + searchTerm?: string, + ): Promise { + const formData = new FormData(); + formData.append("version", "4.0"); + formData.append("uid", "0"); + formData.append("utoken", ""); + formData.append("resource", "general"); + formData.append("action", "get_bundle"); + formData.append("bundle", "bundle_books"); + formData.append("target", "feed"); + formData.append("page", page.toString()); + formData.append("filter_category_id", filters?.category?.value || "0"); + formData.append("filter_completed", "-1"); + formData.append("filter_search", searchTerm || ""); + formData.append("filter_tags", filters?.tags?.value || ""); + formData.append( + "filter_sort", + showLatestNovels ? "new" : filters?.sort?.value || "popular", + ); + formData.append("filter_timeread", filters?.timeread?.value || "0-999999"); + + const result = await fetchApi("https://api.neobook.org/", { + method: "post", + body: formData, + }); + const json = (await result.json()) as { bundle_books: BundleBooks }; + const novels: Plugin.NovelItem[] = []; + + if (json.bundle_books.feed?.length) { + json.bundle_books.feed?.forEach((novel) => + novels.push({ + name: novel.title, + cover: novel?.attachment?.image?.m || defaultCover, + path: novel.token + "/", + }), + ); + } + + return novels; + } + + popularNovels = this.fetchNovels; + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const defaultOptions: any = { + filters: undefined, + showLatestNovels: false, + }; + return this.fetchNovels(page, defaultOptions, searchTerm); + } + + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.resolveUrl(novelPath, true)) + .then((res) => res.text()); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: "", + cover: defaultCover, + }; + + const bookRaw = body.match(/var postData = ({.*?});/); + if (bookRaw instanceof Array && bookRaw.length >= 2) { + const book = JSON.parse(bookRaw[1]) as Novels; + + novel.name = book.title; + novel.summary = book.text?.replace?.(/
/g, "\n") || book.text_fix; + novel.author = + book.user && book.user.firstname && book.user.lastname + ? book.user.firstname + " " + book.user.lastname + : book.user?.initials || ""; + novel.status = statusKey[book.status || "0"] || NovelStatus.Unknown; + + if (book.attachment?.image?.m) novel.cover = book.attachment.image.m; + if (book.tags?.length) novel.genres = book.tags.join(","); + + const chapters: Plugin.ChapterItem[] = []; + + book.chapters?.forEach((chapter, chapterIndex) => { + if (chapter.access == "1" && chapter.status == "1") { + chapters.push({ + name: chapter.title || "Глава " + (chapterIndex + 1), + path: `?book=${book.token}&chapter=${chapter.token}`, + releaseTime: null, + chapterNumber: Number(chapter.sort) || chapterIndex + 1, + }); + } + }); + + novel.chapters = chapters; + } + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.resolveUrl(chapterPath)) + .then((res) => res.text()); + + const bookRaw = body.match(/var data = ({.*?});/); + let chapterText = ""; + + if (bookRaw instanceof Array && bookRaw.length >= 2) { + const book = JSON.parse(bookRaw[1]) as Novels; + const token = chapterPath.split("=")[2]; + const chapter = book.chapters?.find?.( + (chapter) => chapter.token == token, + ); + chapterText = (chapter?.data?.html || "").replace(/
/g, ""); + } + + return chapterText; + } + + fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? "/book/" : "/reader/") + path; + + filters = { + sort: { + label: "Сортировка:", + value: "popular", + options: [ + { label: "Сначала популярные", value: "popular" }, + { label: "Сначала новые", value: "new" }, + { label: "В случайном порядке", value: "rand" }, + ], + type: FilterTypes.Picker, + }, + timeread: { + label: "Время прочтения:", + value: "0-999999", + options: [ + { label: "Все", value: "0-999999" }, + { label: "Менее 15 минут", value: "0-900" }, + { label: "15-30 минут", value: "900-1800" }, + { label: "30-60 минут", value: "1800-3600" }, + { label: "1-2 часа", value: "3600-7200" }, + { label: "Более 2 часов", value: "7200-999999" }, + ], + type: FilterTypes.Picker, + }, + category: { + label: "Жанр:", + value: "", + options: [ + { label: "Все", value: "" }, + { label: "Антиутопия", value: "10" }, + { label: "Детектив", value: "13" }, + { label: "Детские книги", value: "14" }, + { label: "Драма", value: "15" }, + { label: "Другое", value: "16" }, + { label: "История", value: "18" }, + { label: "Мелодрама", value: "21" }, + { label: "Мистика", value: "22" }, + { label: "Научная фантастика", value: "23" }, + { label: "Нон-фикшн", value: "35" }, + { label: "Подростки и молодежь", value: "26" }, + { label: "Постапокалипсис", value: "27" }, + { label: "Поэзия", value: "28" }, + { label: "Приключения", value: "29" }, + { label: "Рассказ", value: "34" }, + { label: "Роман", value: "36" }, + { label: "Творчество", value: "40" }, + { label: "Триллер", value: "91" }, + { label: "Ужасы", value: "90" }, + { label: "Фантастика", value: "41" }, + { label: "Фанфик", value: "42" }, + { label: "Фэнтези", value: "44" }, + { label: "Эротика", value: "46" }, + { label: "Юмор", value: "47" }, + ], + type: FilterTypes.Picker, + }, + tags: { + label: "Тэги:", + value: "", + type: FilterTypes.TextInput, + }, + } satisfies Filters; +} + +export default new Neobook(); + +interface BundleBooks { + categories: any[]; + topSection: any[]; + feed: Novels[]; +} + +interface BottomSection { + id: string; + boost: string; + preferenceid: string; + type_id: string; + contentTypeid: string; + name: string; + title: string; + more: string; + part: string; + items: Novels[]; +} + +interface Novels { + status?: string; + error?: string; + login?: Login; + requestid?: string; + bundleBooks?: BundleBooks; + available?: string; + boosted?: string; + id?: string; + token: string; + type?: string; + typeid?: string; + verified?: string; + audio?: string; + labelid?: string; + labelTitle?: string; + languageid?: string; + languageTitle?: string; + categoryid?: string; + categoryTitle?: string; + price?: Price; + priceSpecial?: Price; + priceDiscount?: string; + title: string; + text: string; + text_fix: string; + copyright?: string; + align?: string; + completed?: string; + timereadS?: string; + views?: string; + likes?: string; + comments?: string; + bookmarks?: string; + isLike?: string; + isBookmark?: string; + isPurchase?: string; + isSelf?: string; + dateEdit?: string; + datePublish?: string; + link_path?: string; + user?: NovelsUser; + attachment?: Attachment; + adult?: string; + adultPlaceholder?: string; + nft?: string; + feed?: Feed; + counters?: NovelsCounters; + rating?: Rating; + readTime?: ReadTime; + readProgress?: ReadProgress; + tags?: string[]; + chapters?: Chapter[]; + lastComments?: any[]; + bottomSection?: BottomSection; + carouselItems?: Novels[]; + book?: Novels; + activeChapterIndex?: number; +} + +interface Attachment { + has: string; + id: string; + typeid: string; + userid: string; + width: string; + height: string; + duration: string; + image: Image; + video: string; + origin: string; + ext: string; +} + +interface Image { + s: string; + m: string; + l: string; + has?: string; +} + +interface Chapter { + id: string; + token: string; + status: string; + sort: string; + free: string; + title: string; + words: string; + access: string; + loaded: string; + data?: Data; +} + +interface Data { + available: string; + access: string; + id: string; + token: string; + postid: string; + free: string; + title: string; + html: string; + dateEdit: string; + datePublish: string; + attachment: Attachment; + lastComments: any[]; +} + +interface NovelsCounters { + impressions: Bookmarks; + views: Bookmarks; + likes: Bookmarks; + comments: Bookmarks; + bookmarks: Bookmarks; +} + +interface Bookmarks { + value: string; + formatted: string; +} + +interface Feed { + name: string; + canRemoved: string; +} + +interface Login { + uid: string; + utoken: string; + has: string; + banned: string; + verified: string; + acc: string; + arm: string; + grm: string; + counters: LoginCounters; + user: LoginUser; + pro: any[]; + earn: any[]; + boost: any[]; + deposit: any[]; + affiliate: any[]; + preferences: any[]; +} + +interface LoginCounters { + notifications: string; + messages: string; +} + +interface LoginUser { + id: string; + username: string; + firstname: string; + lastname: string; + photo: string; + status: string; + balance: Balance; + connectedService: string; +} + +interface Balance { + neo: Price; +} + +interface Price { + has: string; + value: string; + currency: Currency; + formatted: string; + deadline: string; +} + +type Currency = "NEO"; + +interface Rating { + value: string; + count: string; + my: string; +} + +interface ReadProgress { + chapter: string; + progress: string; +} + +interface ReadTime { + hours: string; + minutes: string; +} + +interface NovelsUser { + available: string; + boosted: string; + id: string; + status: string; + verified: string; + metrica: string; + cover: Image; + photo: Image; + username: string; + firstname: string; + lastname: string; + initials: string; + link: string; + about: string; + subscriptions: string; + subscribers: string; + publications: string; + books: string; + poems: string; + posts: string; + drafts: string; + isPro: string; + isAdult: string; + isBanned: string; + isSubscription: string; + isBlock: string; + gr: string; +} diff --git a/plugins/russian/novelOvh.ts b/plugins/russian/novelOvh.ts index 1480cd7..dc73269 100644 --- a/plugins/russian/novelOvh.ts +++ b/plugins/russian/novelOvh.ts @@ -27,7 +27,7 @@ class novelOvh implements Plugin.PluginBase { novels.push({ name: novel.name.ru, cover: novel.poster, - path: "/novel/" + novel.slug, + path: novel.slug, }), ); @@ -36,7 +36,7 @@ class novelOvh implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const result = await fetchApi( - this.site + novelPath + "?_data=routes/reader/book/$slug/index", + this.resolveUrl(novelPath, true) + "?_data=routes/reader/book/$slug/index", ); const json = (await result.json()) as responseNovel; @@ -82,7 +82,7 @@ class novelOvh implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { const result = await fetchApi( - "https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[3], + "https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[1], ); 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, - path: "/novel/" + novel.slug, + path: novel.slug, }), ); @@ -154,6 +154,7 @@ class novelOvh implements Plugin.PluginBase { }; fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path; filters = { sort: { diff --git a/plugins/russian/novelTL.ts b/plugins/russian/novelTL.ts index beb2a14..36af083 100644 --- a/plugins/russian/novelTL.ts +++ b/plugins/russian/novelTL.ts @@ -212,6 +212,7 @@ class TL implements Plugin.PluginBase { } fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => "https://" + path; filters = { tags: { diff --git a/plugins/russian/ranobehub.ts b/plugins/russian/ranobehub.ts index 0b3b5c8..c153211 100644 --- a/plugins/russian/ranobehub.ts +++ b/plugins/russian/ranobehub.ts @@ -67,7 +67,7 @@ class RNBH implements Plugin.PluginBase { novels.push({ name: novel.names.rus || novel.names.eng || novel.names.original, cover: novel.poster.medium, - path: "/ranobe/" + novel.id, + path: novel.id.toString(), }), ); @@ -75,7 +75,7 @@ class RNBH implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + "/api" + novelPath); + const result = await fetchApi(this.site + "/api/ranobe/" + novelPath); const json = (await result.json()) as { data: responseNovel }; const novel: Plugin.SourceNovel = { @@ -98,7 +98,7 @@ class RNBH implements Plugin.PluginBase { const chapters: Plugin.ChapterItem[] = []; const chaptersRaw = await fetchApi( - this.site + "/api" + novelPath + "/contents", + this.site + "/api/ranobe/" + novelPath + "/contents", ); const chaptersJSON = (await chaptersRaw.json()) as { volumes: VolumesEntity[]; @@ -123,7 +123,7 @@ class RNBH implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); + const result = await fetchApi(this.resolveUrl(chapterPath)); const body = await result.text(); const loadedCheerio = parseHTML(body); @@ -161,7 +161,7 @@ class RNBH implements Plugin.PluginBase { novel.name || novel?.names?.original || "", - path: "/ranobe/" + novel.id, + path: novel.id.toString(), cover: novel?.image?.replace("/small", "/medium"), }), ); @@ -170,6 +170,7 @@ class RNBH implements Plugin.PluginBase { } fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => this.site + "/ranobe/" + path; filters = { sort: { diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index 4713d04..b7bbcff 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -67,7 +67,8 @@ class RLIB implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.resolveUrl(novelPath, true)) + .then((res) => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { @@ -104,6 +105,10 @@ class RLIB implements Plugin.PluginBase { } }); + this.ui = loadedCheerio("a.header-right-menu__item") + .attr("href") + ?.replace?.(/[^0-9]/g, ""); + const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/); if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) { const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]); @@ -136,7 +141,7 @@ class RLIB implements Plugin.PluginBase { customOrder[chapter.branch_id || 0] = (customOrder[chapter.branch_id || 0] || 1) + 1; }); - }; + } const chapters: Plugin.ChapterItem[] = []; chaptersJson.chapters.list.forEach((chapter, chapterIndex) => @@ -146,7 +151,7 @@ class RLIB implements Plugin.PluginBase { " Глава " + chapter.chapter_number + (chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""), path: - "/" + chaptersJson.manga.slug + + novelPath + "/v" + chapter.chapter_volume + "/c" + chapter.chapter_number + "?bid=" + (chapter.branch_id || ""), @@ -164,16 +169,15 @@ class RLIB implements Plugin.PluginBase { 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(chapterPath: string): Promise { - const result = await fetchApi( - this.site + chapterPath + (this.ui ? "&ui=" + this.ui : ""), - ).then((res) => res.text()); + const result = await fetchApi(this.resolveUrl(chapterPath)) + .then((res) => res.text()); const loadedCheerio = parseHTML(result); loadedCheerio(".reader-container img").each((index, element) => { @@ -211,6 +215,8 @@ class RLIB implements Plugin.PluginBase { } fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + path + (this.ui ? (isNovel ? "?" : "&") + "ui=" + this.ui : ""); filters = { sort: { diff --git a/plugins/russian/renovels.ts b/plugins/russian/renovels.ts index c64b0f9..4ce55b4 100644 --- a/plugins/russian/renovels.ts +++ b/plugins/russian/renovels.ts @@ -44,19 +44,18 @@ 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), - path: "/novel/" + novel.dir, + path: novel.dir, })); return novels; } - async parseNovel(novelPath: string): Promise { - const novelID = novelPath.split("/")[2]; + async parseNovel(novelID: string): Promise { const result = await fetchApi(this.site + "/api/titles/" + novelID); const body = (await result.json()) as { content: responseNovel }; const novel: Plugin.SourceNovel = { - path: novelPath, + path: novelID, name: body.content.main_name || body.content.secondary_name || @@ -104,7 +103,7 @@ class ReN implements Plugin.PluginBase { "Том " + chapter.tome + " Глава " + chapter.chapter + (chapter.name ? " " + chapter.name.trim() : ""), - path: "/novel/" + novelID + "/" + chapter.id, + path: novelID + "/" + chapter.id, releaseTime: dayjs(chapter.upload_date).format("LLL"), chapterNumber: chapter.index, }); @@ -117,7 +116,7 @@ class ReN implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[3]; + const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[1]; const result = await fetchApi(url); const body = (await result.json()) as { content: responseСhapter }; @@ -137,7 +136,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), - path: "/novel/" + novel.dir, + path: novel.dir, }), ); @@ -145,6 +144,7 @@ class ReN implements Plugin.PluginBase { } fetchImage = fetchFile; + resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path; filters = { sort: { diff --git a/scripts/multisrc/readwn/template.ts b/scripts/multisrc/readwn/template.ts index a0e7abd..5b028cd 100644 --- a/scripts/multisrc/readwn/template.ts +++ b/scripts/multisrc/readwn/template.ts @@ -4,6 +4,7 @@ import { Plugin } from "@typings/plugin"; import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; import dayjs from "dayjs"; +import qs from "qs"; export interface ReadwnMetadata { id: string; @@ -191,9 +192,12 @@ class ReadwnPlugin implements Plugin.PluginBase { Origin: this.site, }, method: "POST", - body: - "show=title&tempid=1&tbname=news&keyboard=" + - encodeURIComponent(searchTerm), + body: qs.stringify({ + show: "title", + tempid: 1, + tbname: "news", + keyboard: searchTerm, + }), }).then((res) => res.text()); const loadedCheerio = parseHTML(result); @@ -205,7 +209,6 @@ class ReadwnPlugin implements Plugin.PluginBase { })) .get() .filter((novel) => novel.name && novel.path); - return novels; } diff --git a/test_web/api/plugins.ts b/test_web/api/plugins.ts index 866abdd..80b5b21 100644 --- a/test_web/api/plugins.ts +++ b/test_web/api/plugins.ts @@ -87,3 +87,14 @@ export const parseChapter = async ( export const fetchImage = async (pluginRequirePath: string, url: string) => (await getPlugin(pluginRequirePath))?.fetchImage(url); + +export const resolveUrl = async ( + pluginRequirePath: string, + path: string, + isNovel?: boolean, +) => { + const plugin = await getPlugin(pluginRequirePath); + if (!plugin) return path; + if (plugin.resolveUrl) return plugin.resolveUrl(path, isNovel); + return plugin.site + path; +}; \ No newline at end of file diff --git a/test_web/index.html b/test_web/index.html index 9eb1cfa..b14c35d 100644 --- a/test_web/index.html +++ b/test_web/index.html @@ -357,6 +357,27 @@ + + + resolveUrl + + +
+ + +
+ +
+
+
+ Loading... +
+ +
+
diff --git a/test_web/static/js/accordion.js b/test_web/static/js/accordion.js index 9cd063a..01534c2 100644 --- a/test_web/static/js/accordion.js +++ b/test_web/static/js/accordion.js @@ -39,7 +39,6 @@ class AccordionHeader extends HTMLElement { return this.parentElement; } connectedCallback() { - console.log("I AM ON THE SCREEN, DO YOUR FUCKING JOB"); const childNodes = Array.from(this.childNodes); this.innerHTML = ""; this.btn.append(...childNodes); diff --git a/test_web/static/js/index.js b/test_web/static/js/index.js index f772d19..c13fe97 100644 --- a/test_web/static/js/index.js +++ b/test_web/static/js/index.js @@ -58,6 +58,8 @@ const parseChapter = $("#parseChapter"); /** @type {JQuery} */ const fetchImage = $("#fetchImage"); +/** @type {JQuery} */ +const resolveUrl = $("#resolveUrl"); /** @type {JQuery}*/ const plugin_language_selection = $("#language"); @@ -1149,6 +1151,37 @@ class PluginWrapper { spinner.hide(); } } + + async resolveUrl() { + const type = $("#resolveUrl select").val(); + const path = $("#resolveUrl input").val(); + const spinner = $("#resolveUrl .spinner-border"); + spinner.show(); + try { + const fullURL = await ( + await fetchFromAPI("/resolveUrl/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + pluginRequirePath: this.requirePath, + isNovel: type === "novel", + path, + }), + }) + ).text(); + $("#resolveUrl input[readonly]").val(fullURL) + } catch (e) { + console.error(e); + if (e) + $("#resolveUrl").text( + `${typeof e === "object" && "message" in e ? e.message : e}` + ); + } finally { + spinner.hide(); + } + } } const currentWrapper = () => state.current_plugin || emptyPluginWrapper; const emptyPluginWrapper = new PluginWrapper(""); @@ -1566,6 +1599,7 @@ $(".parsePage-btn").on("click", () => $(".parseChapter-btn").on("click", () => state.current_plugin?.getChapter()); $(".fetchImage-btn").on("click", () => state.current_plugin?.fetchImage()); +$(".resolveUrl-btn").on("click", () => state.current_plugin?.resolveUrl()); // #endregion diff --git a/types/plugin.ts b/types/plugin.ts index 7ba03db..a259843 100644 --- a/types/plugin.ts +++ b/types/plugin.ts @@ -92,6 +92,7 @@ export namespace Plugin { * ``` */ fetchImage(url: string): Promise; + resolveUrl?(path: string, isNovel?: boolean): string; } export interface PagePlugin extends PluginBase { @@ -104,4 +105,4 @@ export namespace Plugin { */ parsePage(novelPath: string, page: string): Promise; } -} +} \ No newline at end of file