* fix

* test

* sync

* nitromanga -> madara

* test wuxiaworld

* xm

* add chapterNumber

* fix

chapterNumber > 0

* test filters

* nice

* tunovelaligera -> madara

* Squashed commit of the following:

commit 4c7bee5855
Author: nyagami <hoangquan05112002@gmail.com>
Date:   Mon Jan 29 17:56:23 2024 +0700

    fix: use english lang in plugin item consistent

commit b26d1129de
Author: uy/sun <hmy0119@hotmail.com>
Date:   Sun Jan 28 20:22:43 2024 +0800

    Chinese: support showLatestNovels (#733)

    * improve: better chapterText

    * feat: support latest page

commit 6ff206eab3
Author: nyagami <hoangquan05112002@gmail.com>
Date:   Sat Jan 27 09:26:19 2024 +0700

    Refactor: Using English key (#728)

    * fix: use english key instead of native

    * fix: ukrainian

    * fix: web test with new plugins map

commit 62fc988019
Author: Marcin Majewski <skilletssgames@gmail.com>
Date:   Fri Jan 26 14:42:54 2024 +0100

    Fix search box styles (#727)

commit 3a1e0c43eb
Author: uy/sun <hmy0119@hotmail.com>
Date:   Fri Jan 26 19:46:47 2024 +0800

    Add 69shu and 69xinshu (#726)

    * fix(linovelib):  use NovelStatus and translate filter label

    * feat(69xinshu): add 69xinshu

    * feat(69shu): add 69shu

    * remove: empty userAgent

* updated the filters and removed dead sites.

Error: net::ERR_NAME_NOT_RESOLVED at https://cratenovel.com/novel
Error: net::ERR_CONNECTION_REFUSED at https://clicknovel.net/novel
Error: net::ERR_NAME_NOT_RESOLVED at https://noobchan.xyz/novel
Error: net::ERR_CONNECTION_TIMED_OUT at https://sugarbbscan.com/series

* test

* Squashed commit of the following:

commit cadf7a72fe
Author: Felipe Bonfim <felipebonfim2006@gmail.com>
Date:   Tue Jan 30 14:55:34 2024 -0400

    kiniga added (#739)

    * kiniga added

    * kiniga updated icon

commit 38fc1860da
Author: K1ngfish3r <trulyihu@gmail.com>
Date:   Mon Jan 29 23:27:42 2024 +0800

    (add src) LNMTL & ScribbleHub (#738)

    * quick

    * quick panda fix

    * remove console.log

commit 4c7bee5855
Author: nyagami <hoangquan05112002@gmail.com>
Date:   Mon Jan 29 17:56:23 2024 +0700

    fix: use english lang in plugin item consistent

commit b26d1129de
Author: uy/sun <hmy0119@hotmail.com>
Date:   Sun Jan 28 20:22:43 2024 +0800

    Chinese: support showLatestNovels (#733)

    * improve: better chapterText

    * feat: support latest page

commit 6ff206eab3
Author: nyagami <hoangquan05112002@gmail.com>
Date:   Sat Jan 27 09:26:19 2024 +0700

    Refactor: Using English key (#728)

    * fix: use english key instead of native

    * fix: ukrainian

    * fix: web test with new plugins map

commit 62fc988019
Author: Marcin Majewski <skilletssgames@gmail.com>
Date:   Fri Jan 26 14:42:54 2024 +0100

    Fix search box styles (#727)

commit 3a1e0c43eb
Author: uy/sun <hmy0119@hotmail.com>
Date:   Fri Jan 26 19:46:47 2024 +0800

    Add 69shu and 69xinshu (#726)

    * fix(linovelib):  use NovelStatus and translate filter label

    * feat(69xinshu): add 69xinshu

    * feat(69shu): add 69shu

    * remove: empty userAgent

* add wuxiamtl, fannovel

* fix

* Update ranobelib.ts

* parseMadaraDate deleted

* remove multisrc name from Plugin item

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
This commit is contained in:
Rider21
2024-02-04 08:47:12 +03:00
committed by GitHub
parent 26bdb94100
commit fc52a6e665
36 changed files with 34403 additions and 15118 deletions
+1
View File
@@ -17,6 +17,7 @@ fi
git merge $current --strategy-option theirs git merge $current --strategy-option theirs
npm run clearMulti
npm run generate npm run generate
npm run json npm run json
git add . git add .

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

-26
View File
@@ -1,26 +0,0 @@
import dayjs from "dayjs";
export const parseMadaraDate = (date: string) => {
if (date.includes("ago")) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
const timeAgoInt = parseInt(timeAgo, 10);
if (!timeAgo) return date; // 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
}
return dayJSDate.format("LL");
}
return date; // there is no "ago" so don't mess with the date
};
+64 -45
View File
@@ -2,7 +2,7 @@ import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { parseMadaraDate } from "@libs/parseMadaraDate"; import dayjs from "dayjs";
class ComradeMaoPlugin implements Plugin.PluginBase { class ComradeMaoPlugin implements Plugin.PluginBase {
id = "comrademao"; id = "comrademao";
@@ -20,14 +20,14 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
const novelName = loadedCheerio(this).find(".tt").text().trim(); const novelName = loadedCheerio(this).find(".tt").text().trim();
const novelCover = loadedCheerio(this).find("img").attr("src"); const novelCover = loadedCheerio(this).find("img").attr("src");
const novelUrl = loadedCheerio(this).find("a").attr("href"); const novelUrl = loadedCheerio(this).find("a").attr("href");
if (!novelUrl) return; if (!novelUrl) return;
const novel = { const novel = {
name: novelName, name: novelName,
cover: novelCover, cover: novelCover,
url: novelUrl, url: novelUrl,
} };
novels.push(novel); novels.push(novel);
}); });
return novels; return novels;
@@ -35,7 +35,7 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
async popularNovels( async popularNovels(
pageNo: number, pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters> { filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
let link = this.site; let link = this.site;
@@ -43,25 +43,19 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
link += "page/" + pageNo + "/?post_type=novel"; link += "page/" + pageNo + "/?post_type=novel";
const headers = new Headers(); const body = await fetchApi(link).then((result) => result.text());
const body = await fetchApi(link, { headers }).then((result) =>
result.text()
);
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio); return this.parseNovels(loadedCheerio);
} }
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> { async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const url = novelUrl; const body = await fetchApi(novelUrl).then((result) => result.text());
const headers = new Headers();
const result = await fetchApi(url, { headers });
const body = await result.text();
let loadedCheerio = parseHTML(body); let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
url, url: novelUrl,
chapters: [], chapters: [],
}; };
@@ -69,23 +63,23 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
novel.cover = loadedCheerio("div.thumbook > div > img").attr("src"); novel.cover = loadedCheerio("div.thumbook > div > img").attr("src");
loadedCheerio(".infox .wd-full").each((i,el) => { loadedCheerio(".infox .wd-full").each((i, el) => {
const detailName = loadedCheerio(el).find("b").text(); const detailName = loadedCheerio(el).find("b").text();
const detail = loadedCheerio(el).find('span').text(); const detail = loadedCheerio(el).find("span").text();
switch (detailName) { switch (detailName) {
case 'Publisher: ': case "Publisher: ":
novel.author = detail.trim(); novel.author = detail.trim();
break; break;
case 'Status: ': case "Status: ":
novel.status = detail.trim(); novel.status = detail.trim();
break; break;
case 'Genres:': case "Genres:":
novel.genres = detail.trim().replace(/ , /g, ','); novel.genres = detail.trim().replace(/ , /g, ",");
break; break;
case 'Description: ': case "Description: ":
novel.summary = detail.trim(); novel.summary = detail.trim();
break; break;
} }
}); });
@@ -94,53 +88,78 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
loadedCheerio("#chapterlist") loadedCheerio("#chapterlist")
.find("li") .find("li")
.each(function () { .each(function () {
const releaseDate = parseMadaraDate( const chapterUrl = loadedCheerio(this).find("a").attr("href");
loadedCheerio(this).find(".chapterdate").text() if (!chapterUrl) return;
);
const chapterName = loadedCheerio(this) const chapterName = loadedCheerio(this)
.find(".chapternum") .find(".chapternum")
.text(); .text();
const chapterUrl = loadedCheerio(this).find("a").attr("href"); let releaseTime = loadedCheerio(this)
.find(".chapterdate")
.text();
if (!chapterUrl) return; if (releaseTime?.includes?.("ago")) {
const timeAgo = releaseTime.match(/\d+/)?.[0] || "0";
const timeAgoInt = parseInt(timeAgo, 10);
if (timeAgoInt) {
const dayJSDate = dayjs(); // today
if (
releaseTime.includes("hours ago") ||
releaseTime.includes("hour ago")
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
}
if (
releaseTime.includes("days ago") ||
releaseTime.includes("day ago")
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
}
if (
releaseTime.includes("months ago") ||
releaseTime.includes("month ago")
) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
}
releaseTime = dayJSDate.format("LL");
}
}
chapter.push({ chapter.push({
name: chapterName, name: chapterName,
url: chapterUrl, url: chapterUrl,
releaseTime: releaseDate, releaseTime,
}); });
}); });
novel.chapters = chapter.reverse(); novel.chapters = chapter.reverse();
return novel; return novel;
}; }
async parseChapter(chapterUrl: string): Promise<string> { async parseChapter(chapterUrl: string): Promise<string> {
const headers = new Headers(); const body = await fetchApi(chapterUrl).then((result) => result.text());
const result = await fetchApi(chapterUrl, { headers });
const body = await result.text();
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || ""; const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || "";
return chapterText; return chapterText;
}; }
async searchNovels( async searchNovels(
searchTerm: string, searchTerm: string,
pageNo: number pageNo: number,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`; const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
const headers = new Headers(); const body = await fetchApi(url).then((result) => result.text());
const result = await fetchApi(url, { headers });
const body = await result.text();
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio); return this.parseNovels(loadedCheerio);
}; }
async fetchImage(url: string): Promise<string | undefined> { async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url); return await fetchFile(url);
-165
View File
@@ -1,165 +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 Foxaholic implements Plugin.PluginBase {
id = "foxaholic";
name = "Foxaholic";
icon = "src/en/foxaholic/icon.png";
site = "https://www.foxaholic.com/";
filters?: Filters | undefined;
version = "1.0.0";
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const link = `${this.baseUrl}novel/page/${pageNo}/?m_orderby=rating`;
const result = await fetchApi(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".page-item-detail").each(function () {
const novelName = loadedCheerio(this).find(".h5 > a").text();
const novelCover = loadedCheerio(this).find("img").attr("data-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;
}
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(".post-title > h1").text().trim();
novel.cover = loadedCheerio(".summary_image > a > img").attr(
"data-src"
);
loadedCheerio(".post-content_item").each(function () {
const detailName = loadedCheerio(this)
.find(".summary-heading > h5")
.text()
.trim();
const detail = loadedCheerio(this).find(".summary-content").html();
if (!detail) return;
switch (detailName) {
case "Genre":
novel.genres = loadedCheerio(detail)
.children("a")
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
break;
case "Author":
novel.author = loadedCheerio(detail)
.children("a")
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(", ");
break;
case "Novel":
novel.status = detail?.trim().replace(/G/g, "g");
break;
}
});
loadedCheerio(
".description-summary > div.summary__content > div"
).remove();
novel.summary = loadedCheerio(
".description-summary > div.summary__content"
)
.text()
.trim();
let chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".wp-manga-chapter").each(function () {
const chapterName = loadedCheerio(this)
.find("a")
.text()
.replace(/[\t\n]/g, "")
.trim();
const releaseDate = loadedCheerio(this).find("span").text().trim();
const chapterUrl = loadedCheerio(this).find("a").attr("href");
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: releaseDate,
url: chapterUrl,
});
});
novel.chapters = chapter.reverse();
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const result = await fetchApi(chapterUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio("img").removeAttr("srcset");
let chapterText = loadedCheerio(".reading-content").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".c-tabs-item__content").each(function () {
const novelName = loadedCheerio(this).find(".h4 > a").text();
const novelCover = loadedCheerio(this).find("img").attr("data-src");
const novelUrl = loadedCheerio(this).find(".h4 > a").attr("href");
if (!novelUrl) return;
novels.push({
name: novelName,
url: novelUrl,
cover: novelCover,
});
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new Foxaholic();
+108 -103
View File
@@ -1,103 +1,108 @@
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
class FreeWebNovel implements Plugin.PluginBase { class FreeWebNovel implements Plugin.PluginBase {
id = "FWN.com"; id = "FWN.com";
name = "Web NOVEL"; name = "Web NOVEL";
site = "https://freewebnovel.com"; site = "https://freewebnovel.com";
version = "1.0.0"; version = "1.0.0";
icon = "src/en/freewebnovel/icon.png"; icon = "src/en/freewebnovel/icon.png";
async popularNovels(
page: number,
async popularNovels( { showLatestNovels }: Plugin.PopularNovelsOptions,
page: number, ): Promise<Plugin.NovelItem[]> {
{ showLatestNovels }: Plugin.PopularNovelsOptions, const sort = showLatestNovels
): Promise<Plugin.NovelItem[]> { ? "/latest-release-novels/"
const sort = showLatestNovels : "/completed-novels/";
? "/latest-release-novels/"
: "/completed-novels/"; const body = await fetchApi(this.site + sort + page).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const body = await fetchApi(this.site + sort + page).then((res) => res.text());
const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = loadedCheerio(".li-row")
.map((index, element) => ({
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row") name: loadedCheerio(element).find(".tit").text(),
.map((index, element) => ({ cover: loadedCheerio(element).find("img").attr("src"),
name: loadedCheerio(element).find(".tit").text(), url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
cover: loadedCheerio(element).find("img").attr("src"), }))
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"), .get();
}))
.get(); return novels;
}
return novels;
} async parseNovelAndChapters(url: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(url).then((res) => res.text());
async parseNovelAndChapters(url: string): Promise<Plugin.SourceNovel> { const loadedCheerio = parseHTML(body);
const body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { url };
novel.name = loadedCheerio("h1.tit").text();
const novel: Plugin.SourceNovel = { url }; novel.cover = loadedCheerio(".pic > img").attr("src");
novel.name = loadedCheerio("h1.tit").text(); novel.genres = loadedCheerio("[title=Genre]")
novel.cover = loadedCheerio(".pic > img").attr("src"); .next()
novel.genres = loadedCheerio("[title=Genre]") .text()
.next() .replace(/[\t\n]/g, "");
.text()
.replace(/[\t\n]/g, ""); novel.author = loadedCheerio("[title=Author]")
.next()
novel.author = loadedCheerio("[title=Author]") .text()
.next() .replace(/[\t\n]/g, "");
.text()
.replace(/[\t\n]/g, ""); novel.status = loadedCheerio("[title=Status]")
.next()
novel.status = loadedCheerio("[title=Status]") .text()
.next() .replace(/[\t\n]/g, "");
.text()
.replace(/[\t\n]/g, ""); novel.summary = loadedCheerio(".inner").text().trim();
novel.summary = loadedCheerio(".inner").text().trim(); const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a")
.map((chapterIndex, element) => ({
const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a") name: loadedCheerio(element).attr("title") || "Chapter " + (chapterIndex + 1),
.map((index, element) => ({ url: this.site + loadedCheerio(element).attr("href"),
name: loadedCheerio(element).attr("title") || "Chapter " + index, releaseTime: null,
releaseTime: null, chapterNumber: chapterIndex + 1,
url: this.site + loadedCheerio(element).attr("href"), }))
})) .get();
.get();
novel.chapters = chapters;
novel.chapters = chapters; return novel;
return novel; }
}
async parseChapter(chapterUrl: string): Promise<string> {
async parseChapter(chapterUrl: string): Promise<string> { const body = await fetchApi(chapterUrl).then((res) => res.text());
const body = await fetchApi(chapterUrl).then((res) => res.text()); const loadedCheerio = parseHTML(body);
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("div.txt").html() || "";
const chapterText = loadedCheerio("div.txt").html() || ""; return chapterText;
return chapterText; }
}
async searchNovels(
async searchNovels( searchTerm: string,
searchkey: string, ): Promise<Plugin.NovelItem[]> {
): Promise<Plugin.NovelItem[]> { const body = await fetchApi(this.site + "/search/", {
const body = await fetchApi(this.site + "/search/", { headers: {
method: "POST", "Content-Type": "application/x-www-form-urlencoded",
body: JSON.stringify({ searchkey }), Referer: this.site,
}).then((res) => res.text()); Origin: this.site,
},
const loadedCheerio = parseHTML(body); method: "POST",
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con") body: "searchkey=" + encodeURIComponent(searchTerm)
.map((index, element) => ({ }).then((res) => res.text());
name: loadedCheerio(element).find(".tit").text(),
cover: loadedCheerio(element).find(".pic > a > img").attr("data-cfsrc"), const loadedCheerio = parseHTML(body);
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"), const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con")
})) .map((index, element) => ({
.get(); name: loadedCheerio(element).find(".tit").text(),
cover: loadedCheerio(element).find(".pic > a > img").attr("src"),
return novels; url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
} }))
.get();
fetchImage = fetchFile;
} return novels;
}
fetchImage = fetchFile;
}
export default new FreeWebNovel();
-153
View File
@@ -1,153 +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 LightNovelUpdates implements Plugin.PluginBase {
id = "LightNovelUpdates";
name = "Light Novel Updates";
version = "1.0.0";
icon = "src/en/lightnovelupdates/icon.png";
site = "https://www.lightnovelupdates.com/";
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
let url = `${this.baseUrl}novel/page/${pageNo}/?m_orderby=rating`;
const body = await fetchApi(url).then((r) => r.text());
const loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".page-item-detail").each(function () {
const novelName = loadedCheerio(this).find(".h5 > a").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;
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const url = novelUrl;
const body = await fetchApi(url).then((r) => r.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
url,
chapters: [],
};
novel.name = loadedCheerio(".post-title > h1").text().trim();
novel.cover =
loadedCheerio(".summary_image > a > img").attr("src") || undefined;
loadedCheerio(".post-content_item").each(function () {
const detailName = loadedCheerio(this)
.find(".summary-heading > h5")
.text()
.trim();
const detail = loadedCheerio(this)
.find(".summary-content")
.text()
.trim();
switch (detailName) {
case "Genre(s)":
novel.genres = detail.trim().replace(/[\t\n]/g, ",");
break;
case "Author(s)":
novel.author = detail.trim();
break;
case "Status":
novel.status = detail.replace(/G/g, "g");
break;
}
});
novel.summary = loadedCheerio("div.summary__content").text();
const chapter: Plugin.ChapterItem[] = [];
const novelId = loadedCheerio(".rating-post-id").attr("value")!;
let formData = new FormData();
formData.append("action", "manga_get_chapters");
formData.append("manga", novelId);
const data = await fetchApi(
"https://www.lightnovelupdates.com/wp-admin/admin-ajax.php",
{
method: "POST",
body: formData,
},
);
const text = await data.text();
loadedCheerio = parseHTML(text);
loadedCheerio(".wp-manga-chapter").each(function () {
const chapterName = loadedCheerio(this).find("a").text().trim();
const releaseDate = loadedCheerio(this).find("span").text().trim();
const chapterUrl = loadedCheerio(this).find("a").attr("href");
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: releaseDate,
url: chapterUrl,
});
});
novel.chapters = chapter.reverse();
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const chapterText = 'unable to load';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga&m_orderby=rating`;
const body = await fetchApi(url).then((r) => r.text());
const loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".c-tabs-item__content").each(function () {
const novelName = loadedCheerio(this).find(".h4 > a").text();
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelUrl = loadedCheerio(this).find(".h4 > a").attr("href");
if (!novelUrl) return;
novels.push({
url: novelUrl,
name: novelName,
cover: novelCover,
});
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new LightNovelUpdates();
-154
View File
@@ -1,154 +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 NitroManga implements Plugin.PluginBase {
id = "nitromanga";
name = "Nitro Manga";
version = "1.0.0";
icon = "src/en/nitromanga/icon.png";
filters?: Filters | undefined;
site = "https://nitromanga.com/";
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const url = this.baseUrl + "mangas-genre/novel/?m_orderby=trending";
const body = await fetchApi(url).then((r) => r.text());
const loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".page-item-detail").each(function () {
const novelName = loadedCheerio(this).find("h3 > a").text();
const novelCover = loadedCheerio(this).find("img").attr("data-src");
const novelUrl = loadedCheerio(this).find("h3 > 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 body = await fetchApi(url).then((r) => r.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
url,
chapters: [],
};
loadedCheerio(".manga-title-badges.custom.novel").remove();
novel.name = loadedCheerio(".post-title > h1").text().trim();
novel.cover = loadedCheerio(".summary_image")
.find("img")
.attr("data-src");
novel.summary = loadedCheerio(".summary__content").text()?.trim();
novel.genres = loadedCheerio(".genres-content")
.children("a")
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
loadedCheerio(".post-content_item").each(function () {
const detailName = loadedCheerio(this)
.find(".summary-heading")
.text()
.trim();
const detail = loadedCheerio(this)
.find(".summary-content")
.text()
.trim();
switch (detailName) {
case "Author(s)":
novel.author = detail;
break;
case "Status":
novel.status = detail.replace(/G/g, "g");
break;
}
});
let chapter: Plugin.ChapterItem[] = [];
let chapterlisturl = novelUrl + "ajax/chapters/";
const data = await fetchApi(chapterlisturl, { method: "POST" });
const text = await data.text();
loadedCheerio = parseHTML(text);
loadedCheerio(".wp-manga-chapter").each(function () {
const chapterName = loadedCheerio(this).find("a").text().trim();
const releaseDate = loadedCheerio(this).find("span").text().trim();
const chapterUrl = loadedCheerio(this).find("a").attr("href");
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: releaseDate,
url: chapterUrl,
});
});
novel.chapters = chapter.reverse();
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((r) => r.text());
let loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio(".text-left").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = this.baseUrl + '?s=' + searchTerm + '&post_type=wp-manga&genre%5B%5D=novel';
const body = await fetchApi(url).then((r) => r.text());
let loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".c-tabs-item__content").each(function () {
const novelName = loadedCheerio(this).find("h3 > a").text();
const novelCover = loadedCheerio(this).find("img").attr("data-src");
const novelUrl = loadedCheerio(this).find("h3 > a").attr("href");
if (!novelUrl) return;
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 NitroManga();
+242 -241
View File
@@ -1,241 +1,242 @@
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
class freedlit implements Plugin.PluginBase { class freedlit implements Plugin.PluginBase {
id = "freedlit.space"; id = "freedlit.space";
name = "LitSpace"; name = "LitSpace";
site = "https://freedlit.space"; site = "https://freedlit.space";
version = "1.0.0"; version = "1.0.0";
icon = "src/ru/freedlit/icon.png"; icon = "src/ru/freedlit/icon.png";
async popularNovels(
page: number,
async popularNovels( { showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
page: number, ): Promise<Plugin.NovelItem[]> {
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>, let url = this.site + "/get-books/all/list/" + page + "?sort=";
): Promise<Plugin.NovelItem[]> { url += showLatestNovels ? "recent" : filters?.sort?.value || "popular";
let url = this.site + "/get-books/all/list/" + page + "?sort="; url += "&status=" + (filters?.status?.value || "all");
url += showLatestNovels ? "recent" : filters?.sort?.value || "popular"; url += "&access=" + (filters?.access?.value || "all");
url += "&status=" + (filters?.status?.value || "all"); url += "&adult=" + (filters?.adult?.value || "hide");
url += "&access=" + (filters?.access?.value || "all");
url += "&adult=" + (filters?.adult?.value || "hide"); if (filters?.genre?.value?.include?.length) {
url += filters.genre.value.include
if (filters?.genre?.value?.include?.length) { .map((id) => "&genres_included[]=" + id)
url += filters.genre.value.include .join("");
.map(id => '&genres_included[]=' + id).join(""); }
}
if (filters?.genre?.value?.exclude?.length) {
if (filters?.genre?.value?.exclude?.length) { url += filters.genre.value.exclude
url += filters.genre.value.exclude .map((id) => "&genres_excluded[]=" + id)
.map(id => '&genres_excluded[]=' + id).join(""); .join("");
} }
const body = await fetchApi(url).then((res) => res.text()); const body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
loadedCheerio("#bookListBlock > div > div").each(function () { loadedCheerio("#bookListBlock > div > div").each(function () {
const name = loadedCheerio(this).find("div > h4 > a").text()?.trim(); const name = loadedCheerio(this).find("div > h4 > a").text()?.trim();
const cover = loadedCheerio(this) const cover = loadedCheerio(this)
.find("div > a > img") .find("div > a > img")
.attr("src") .attr("src")
?.trim(); ?.trim();
const url = loadedCheerio(this).find("div > h4 > a").attr("href")?.trim(); const url = loadedCheerio(this).find("div > h4 > a").attr("href")?.trim();
if (!name || !url) return; if (!name || !url) return;
novels.push({ name, cover, url }); novels.push({ name, cover, url });
}); });
return novels; return novels;
} }
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> { async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((res) => res.text()); const body = await fetchApi(novelUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
url: novelUrl, url: novelUrl,
name: loadedCheerio(".book-info h4").text(), name: loadedCheerio(".book-info h4").text(),
cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(), cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(),
summary: loadedCheerio("#nav-home").text()?.trim(), summary: loadedCheerio("#nav-home").text()?.trim(),
author: loadedCheerio(".book-info h5 > a").text(), author: loadedCheerio(".book-info h5 > a").text(),
genres: loadedCheerio(".genre-list > a") genres: loadedCheerio(".genre-list > a")
.map((index, element) => loadedCheerio(element).text()) .map((index, element) => loadedCheerio(element).text())
.get() .get()
.join(","), .join(","),
}; };
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
loadedCheerio("a.chapter-line").each(function () { loadedCheerio("a.chapter-line").each((chapterIndex, element) => {
const name = loadedCheerio(this).find("h6").text(); const name = loadedCheerio(element).find("h6").text();
const releaseTime = loadedCheerio(this).find('span[class="date"]').text(); const url = loadedCheerio(element).attr("href");
const url = loadedCheerio(this).attr("href"); if (!name || !url) return;
if (!name || !url) return;
const releaseTime = loadedCheerio(element).find('span[class="date"]').text();
chapters.push({ name, releaseTime, url }); chapters.push({ name, url, releaseTime, chapterNumber: chapterIndex + 1 });
}); });
novel.chapters = chapters; novel.chapters = chapters;
return novel; return novel;
} }
async parseChapter(chapterUrl: string): Promise<string> { async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text()); const body = await fetchApi(chapterUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
loadedCheerio("div.mobile-block").remove(); loadedCheerio("div.mobile-block").remove();
loadedCheerio("div.standart-block").remove(); loadedCheerio("div.standart-block").remove();
const chapterText = loadedCheerio('div[class="chapter"]').html() || ""; const chapterText = loadedCheerio('div[class="chapter"]').html() || "";
return chapterText; return chapterText;
} }
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { 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 body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
loadedCheerio("#bookListBlock > div").each(function () { loadedCheerio("#bookListBlock > div").each(function () {
const name = loadedCheerio(this).find("h4 > a").text()?.trim(); const name = loadedCheerio(this).find("h4 > a").text()?.trim();
const cover = loadedCheerio(this).find("a > img").attr("src")?.trim(); const cover = loadedCheerio(this).find("a > img").attr("src")?.trim();
const url = loadedCheerio(this).find("h4 > a").attr("href")?.trim(); const url = loadedCheerio(this).find("h4 > a").attr("href")?.trim();
if (!name || !url) return; if (!name || !url) return;
novels.push({ name, cover, url }); novels.push({ name, cover, url });
}); });
return novels; return novels;
} }
fetchImage = fetchFile; fetchImage = fetchFile;
filters = { filters = {
sort: { sort: {
label: "Сортировка:", label: "Сортировка:",
value: "", value: "popular",
options: [ options: [
{ label: "По популярности", value: "popular" }, { label: "По популярности", value: "popular" },
{ label: "По количеству комментариев", value: "comments" }, { label: "По количеству комментариев", value: "comments" },
{ label: "По количеству лайков", value: "likes" }, { label: "По количеству лайков", value: "likes" },
{ label: "По новизне", value: "recent" }, { label: "По новизне", value: "recent" },
{ label: "По просмотрам", value: "views" }, { label: "По просмотрам", value: "views" },
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
genre: { genre: {
label: "Жанры:", label: "Жанры:",
value: { include: [], exclude: [] }, value: { include: [], exclude: [] },
options: [ options: [
{ label: "Любой жанр", value: "all" }, { label: "Любой жанр", value: "all" },
{ label: "Альтернативная история", value: "alternative-history" }, { label: "Альтернативная история", value: "alternative-history" },
{ label: "Антиутопия", value: "dystopia" }, { label: "Антиутопия", value: "dystopia" },
{ label: "Бизнес-литература", value: "business-literature" }, { label: "Бизнес-литература", value: "business-literature" },
{ label: "Боевая фантастика", value: "combat-fiction" }, { label: "Боевая фантастика", value: "combat-fiction" },
{ label: "Боевик", value: "action" }, { label: "Боевик", value: "action" },
{ label: "Боевое фэнтези", value: "combat-fantasy" }, { label: "Боевое фэнтези", value: "combat-fantasy" },
{ label: "Бояръ-Аниме", value: "boyar-anime" }, { label: "Бояръ-Аниме", value: "boyar-anime" },
{ label: "Героическая фантастика", value: "heroic-fiction" }, { label: "Героическая фантастика", value: "heroic-fiction" },
{ label: "Героическое фэнтези", value: "heroic-fantasy" }, { label: "Героическое фэнтези", value: "heroic-fantasy" },
{ label: "Городское фэнтези", value: "urban-fantasy" }, { label: "Городское фэнтези", value: "urban-fantasy" },
{ label: "Гримдарк", value: "grimdark" }, { label: "Гримдарк", value: "grimdark" },
{ label: "Детектив", value: "mystery" }, { label: "Детектив", value: "mystery" },
{ label: "Детская литература", value: "kids-literature" }, { label: "Детская литература", value: "kids-literature" },
{ label: "Документальная проза", value: "biography" }, { label: "Документальная проза", value: "biography" },
{ label: "Историческая проза", value: "historical-fiction" }, { label: "Историческая проза", value: "historical-fiction" },
{ label: "Исторический детектив", value: "historical-mystery" }, { label: "Исторический детектив", value: "historical-mystery" },
{ label: "Исторический любовный роман", value: "historical-romantic-novel" }, { label: "Исторический любовный роман", value: "historical-romantic-novel" },
{ label: "Историческое фэнтези", value: "historical-fantasy" }, { label: "Историческое фэнтези", value: "historical-fantasy" },
{ label: "Киберпанк", value: "cyberpunk" }, { label: "Киберпанк", value: "cyberpunk" },
{ label: "Космическая фантастика", value: "cosmic-fiction" }, { label: "Космическая фантастика", value: "cosmic-fiction" },
{ label: "ЛитРПГ", value: "litrpg" }, { label: "ЛитРПГ", value: "litrpg" },
{ label: "Лоу / Низкое фэнтези", value: "low-fantasy" }, { label: "Лоу / Низкое фэнтези", value: "low-fantasy" },
{ label: "Любовное фэнтези", value: "romantic-fantasy" }, { label: "Любовное фэнтези", value: "romantic-fantasy" },
{ label: "Любовный роман", value: "romantic-novel" }, { label: "Любовный роман", value: "romantic-novel" },
{ label: "Мистика", value: "mystic" }, { label: "Мистика", value: "mystic" },
{ label: "Мистический детектив", value: "mystic-detective" }, { label: "Мистический детектив", value: "mystic-detective" },
{ label: "Научная фантастика", value: "science-fiction" }, { label: "Научная фантастика", value: "science-fiction" },
{ label: "Подростковая проза", value: "young-adult" }, { label: "Подростковая проза", value: "young-adult" },
{ label: "Политический роман", value: "political-romance" }, { label: "Политический роман", value: "political-romance" },
{ label: "Попаданцы", value: "accidental-travel" }, { label: "Попаданцы", value: "accidental-travel" },
{ label: "Попаданцы в магические миры", value: "magic-worlds-travel" }, { label: "Попаданцы в магические миры", value: "magic-worlds-travel" },
{ label: "Попаданцы во времени", value: "time-travel" }, { label: "Попаданцы во времени", value: "time-travel" },
{ label: "Порнотика", value: "pornotica" }, { label: "Порнотика", value: "pornotica" },
{ label: "Постапокалипсис", value: "post-apocalypse" }, { label: "Постапокалипсис", value: "post-apocalypse" },
{ label: "Поэзия", value: "poetry" }, { label: "Поэзия", value: "poetry" },
{ label: "Приключения", value: "adventure" }, { label: "Приключения", value: "adventure" },
{ label: "Публицистика", value: "journalism" }, { label: "Публицистика", value: "journalism" },
{ label: "Пьеса", value: "play" }, { label: "Пьеса", value: "play" },
{ label: "Развитие личности", value: "how-to-book" }, { label: "Развитие личности", value: "how-to-book" },
{ label: "Разное", value: "other" }, { label: "Разное", value: "other" },
{ label: "Реализм", value: "Realism" }, { label: "Реализм", value: "Realism" },
{ label: "РеалРПГ", value: "realrpg" }, { label: "РеалРПГ", value: "realrpg" },
{ label: "Репликация", value: "replication" }, { label: "Репликация", value: "replication" },
{ label: "Романтическая эротика", value: "romantic-erotic-fiction" }, { label: "Романтическая эротика", value: "romantic-erotic-fiction" },
{ label: "Сказка", value: "fairy-tale" }, { label: "Сказка", value: "fairy-tale" },
{ label: "Слэш", value: "slash" }, { label: "Слэш", value: "slash" },
{ label: "Современная проза", value: "modern-prose" }, { label: "Современная проза", value: "modern-prose" },
{ label: "Современный любовный роман", value: "modern-romantic-novel" }, { label: "Современный любовный роман", value: "modern-romantic-novel" },
{ label: "Социальная фантастика", value: "social-fiction" }, { label: "Социальная фантастика", value: "social-fiction" },
{ label: "Стимпанк", value: "steampunk" }, { label: "Стимпанк", value: "steampunk" },
{ label: "Сценарий", value: "scenario" }, { label: "Сценарий", value: "scenario" },
{ label: "Сюаньхуань", value: "xuanhuan" }, { label: "Сюаньхуань", value: "xuanhuan" },
{ label: "Сянься", value: "xianxia" }, { label: "Сянься", value: "xianxia" },
{ label: "Тёмное фэнтези", value: "dark-fantasy" }, { label: "Тёмное фэнтези", value: "dark-fantasy" },
{ label: "Триллер", value: "thriller" }, { label: "Триллер", value: "thriller" },
{ label: "Уся", value: "wuxia" }, { label: "Уся", value: "wuxia" },
{ label: "Фантастика", value: "fiction" }, { label: "Фантастика", value: "fiction" },
{ label: "Фантастический детектив", value: "mystery-fiction" }, { label: "Фантастический детектив", value: "mystery-fiction" },
{ label: "Фанфик", value: "fan-fiction" }, { label: "Фанфик", value: "fan-fiction" },
{ label: "Фемслэш", value: "femslash" }, { label: "Фемслэш", value: "femslash" },
{ label: "Фэнтези", value: "fantasy" }, { label: "Фэнтези", value: "fantasy" },
{ label: "Хоррор", value: "horror" }, { label: "Хоррор", value: "horror" },
{ label: "Шпионский детектив", value: "spy-crime" }, { label: "Шпионский детектив", value: "spy-crime" },
{ label: "Эпическое фэнтези", value: "epic-fantasy" }, { label: "Эпическое фэнтези", value: "epic-fantasy" },
{ label: "Эротика", value: "erotic-fiction" }, { label: "Эротика", value: "erotic-fiction" },
{ label: "Эротическая фантастика", value: "erotic-fiction" }, { label: "Эротическая фантастика", value: "erotic-fiction" },
{ label: "Эротический фанфик", value: "erotic-fan-fiction" }, { label: "Эротический фанфик", value: "erotic-fan-fiction" },
{ label: "Эротическое фэнтези", value: "erotic-fantasy" }, { label: "Эротическое фэнтези", value: "erotic-fantasy" },
{ label: "Этническое фэнтези", value: "ethnic-fantasy" }, { label: "Этническое фэнтези", value: "ethnic-fantasy" },
{ label: "Юмор", value: "humor" }, { label: "Юмор", value: "humor" },
{ label: "Юмористическая фантастика", value: "humor-fiction" }, { label: "Юмористическая фантастика", value: "humor-fiction" },
{ label: "Юмористическое фэнтези", value: "humor-fantasy" }, { label: "Юмористическое фэнтези", value: "humor-fantasy" },
{ label: "RPS", value: "rps" }, { label: "RPS", value: "rps" },
], ],
type: FilterTypes.ExcludableCheckboxGroup, type: FilterTypes.ExcludableCheckboxGroup,
}, },
status: { status: {
label: "Статус:", label: "Статус:",
value: "", value: "all",
options: [ options: [
{ label: "Любой статус", value: "all" }, { label: "Любой статус", value: "all" },
{ label: "В процессе", value: "in-process" }, { label: "В процессе", value: "in-process" },
{ label: "Завершено", value: "finished" }, { label: "Завершено", value: "finished" },
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
access: { access: {
label: "Доступ:", label: "Доступ:",
value: "", value: "all",
options: [ options: [
{ label: "Любой доступ", value: "all" }, { label: "Любой доступ", value: "all" },
{ label: "Бесплатные", value: "free" }, { label: "Бесплатные", value: "free" },
{ label: "Платные", value: "paid" }, { label: "Платные", value: "paid" },
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
adult: { adult: {
label: "Возрастные ограничения:", label: "Возрастные ограничения:",
value: "", value: "hide",
options: [ options: [
{ label: "Скрыть 18+", value: "hide" }, { label: "Скрыть 18+", value: "hide" },
{ label: "Показать +18", value: "show" }, { label: "Показать +18", value: "show" },
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
} satisfies Filters; } satisfies Filters;
} }
export default new freedlit();
File diff suppressed because it is too large Load Diff
+252 -258
View File
@@ -1,258 +1,252 @@
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
import dayjs from "dayjs"; import dayjs from "dayjs";
class Bookriver implements Plugin.PluginBase { class Bookriver implements Plugin.PluginBase {
id = "bookriver"; id = "bookriver";
name = "Bookriver"; name = "Bookriver";
site = "https://bookriver.ru"; site = "https://bookriver.ru";
version = "1.0.0"; version = "1.0.0";
icon = "src/ru/bookriver/icon.png"; icon = "src/ru/bookriver/icon.png";
async popularNovels(
pageNo: number,
async popularNovels( { showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
pageNo: number, ): Promise<Plugin.NovelItem[]> {
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions, let url = this.site + `/genre?page=${pageNo}&perPage=24&sortingType=`;
): Promise<Plugin.NovelItem[]> { url += showLatestNovels ? "last-update" : filters?.sort?.value || "bestseller";
let url = this.site + `/genre?page=${pageNo}&perPage=24&sortingType=`;
url += showLatestNovels if (filters?.genres?.value?.length) {
? "last-update" url += "&g=" + filters.genres.value.join(",");
: filters?.sort?.value || "bestseller"; }
if (filters?.genres?.value instanceof Array && filters?.genres?.value.length) { const result = await fetchApi(url).then((res) => res.text());
url += "&g=" + filters.genres.value.join(","); const loadedCheerio = parseHTML(result);
} const novels: Plugin.NovelItem[] = [];
const result = await fetchApi(url); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const body = await result.text(); if (jsonRaw) {
const json: response = JSON.parse(jsonRaw);
const loadedCheerio = parseHTML(body); json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) =>
const novels: Plugin.NovelItem[] = []; novels.push({
name: novel.name,
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); cover: novel.coverImages[0].url,
if (jsonRaw) { url: this.site + "/book/" + novel.slug,
const json: response = JSON.parse(jsonRaw); }),
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) => );
novels.push({ }
name: novel.name, return novels;
cover: novel.coverImages[0].url, }
url: this.site + "/book/" + novel.slug,
}), async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
); const result = await fetchApi(novelUrl).then((res) => res.text());
} const loadedCheerio = parseHTML(result);
return novels;
} const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const json: response = JSON.parse(jsonRaw || "{}");
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> { const book = json.props.pageProps.state.book?.bookPage;
const result = await fetchApi(novelUrl);
const body = await result.text(); const novel: Plugin.SourceNovel = {
url: novelUrl,
const loadedCheerio = parseHTML(body); name: book?.name,
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); cover: book?.coverImages[0].url,
const json: response = JSON.parse(jsonRaw || "{}"); summary: book?.annotation,
const book = json.props.pageProps.state.book?.bookPage; author: book?.author?.name,
const novel: Plugin.SourceNovel = { genres: book?.tags?.map((tag) => tag.name).join(", "),
url: novelUrl, status:
name: book?.name, book?.statusComplete === "writing"
cover: book?.coverImages[0].url, ? NovelStatus.Ongoing
summary: book?.annotation, : NovelStatus.Completed,
author: book?.author?.name, };
genres: book?.tags?.map((item) => item.name).join(", "),
status: const chapters: Plugin.ChapterItem[] = [];
book?.statusComplete === "writing" book?.ebook?.chapters?.forEach((chapter, chapterIndex) => {
? NovelStatus.Ongoing if (chapter.available) {
: NovelStatus.Completed, chapters.push({
}; name: chapter.name,
url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId,
const chapters: Plugin.ChapterItem[] = []; releaseTime: dayjs(chapter?.firstPublishedAt || chapter.createdAt).format("LLL"),
book?.ebook?.chapters?.forEach((chapter) => { chapterNumber: chapterIndex + 1,
if (chapter.available) { });
chapters.push({ }
name: chapter.name, });
releaseTime: dayjs( novel.chapters = chapters;
chapter?.firstPublishedAt || chapter.createdAt, return novel;
).format("LLL"), }
url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId,
}); async parseChapter(chapterUrl: string): Promise<string> {
} const url = "https://api.bookriver.ru/api/v1/books/chapter/text/";
}); const result = await fetchApi(url + chapterUrl.split("/").pop());
novel.chapters = chapters; const json = (await result.json()) as responseChapter;
return novel;
} let chapterText = json.data.content || "Конец произведения";
if (json.data?.audio?.available) {
async parseChapter(chapterUrl: string): Promise<string> { chapterText += "\n" + json.data.audio.url;
const url = "https://api.bookriver.ru/api/v1/books/chapter/text/"; }
const result = await fetchApi(url + chapterUrl.split("/").pop());
const json = (await result.json()) as responseChapter; return chapterText;
}
let chapterText = json.data.content || "Конец произведения";
if (json.data?.audio?.available) { async searchNovels(
chapterText += "\n" + json.data.audio.url; searchTerm: string,
} pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
return chapterText; 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;
async searchNovels(
searchTerm: string, const novels: Plugin.NovelItem[] = [];
pageNo: number | undefined = 1, json.data?.books?.forEach((novel) =>
): Promise<Plugin.NovelItem[]> { novels.push({
const url = `https://api.bookriver.ru/api/v1/search/autocomplete?keyword=${searchTerm}&page=${pageNo}&perPage=10`; name: novel.name,
const result = await fetchApi(url); cover: novel.coverImages[0].url,
const json = (await result.json()) as responseSearch; url: this.site + "/book/" + novel.slug,
}),
const novels: Plugin.NovelItem[] = []; );
json?.data?.books?.forEach((novel) =>
novels.push({ return novels;
name: novel.name, }
cover: novel.coverImages[0].url, fetchImage = fetchFile;
url: this.site + "/book/" + novel.slug,
}), filters = {
); sort: {
label: "Сортировка",
return novels; value: "bestseller",
} options: [
fetchImage = fetchFile; { label: "Бестселлеры", value: "bestseller" },
{ label: "Дате добавления", value: "newest" },
filters = { { label: "Дате обновления", value: "last-update" },
sort: { ],
label: "Сортировка", type: FilterTypes.Picker,
value: "", },
options: [ genres: {
{ label: "Бестселлеры", value: "bestseller" }, label: "жанры",
{ label: "Дате добавления", value: "newest" }, value: [],
{ label: "Дате обновления", value: "last-update" }, options: [
], { label: "Альтернативная история", value: "alternativnaya-istoriya" },
type: FilterTypes.Picker, { label: "Боевая фантастика", value: "boevaya-fantastika" },
}, { label: "Боевое фэнтези", value: "boevoe-fentezi" },
genres: { { label: "Бытовое фэнтези", value: "bytovoe-fentezi" },
label: "жанры", { label: "Героическая фантастика", value: "geroicheskaya-fantastika" },
value: [], { label: "Героическое фэнтези", value: "geroicheskoe-fentezi" },
options: [ { label: "Городское фэнтези", value: "gorodskoe-fentezi" },
{ label: "Альтернативная история", value: "alternativnaya-istoriya" }, { label: "Детектив", value: "detektiv" },
{ label: "Боевая фантастика", value: "boevaya-fantastika" }, { label: "Детективная фантастика", value: "detektivnaya-fantastika" },
{ label: "Боевое фэнтези", value: "boevoe-fentezi" }, { label: "Жёсткая эротика", value: "zhyostkaya-erotika" },
{ label: "Бытовое фэнтези", value: "bytovoe-fentezi" }, { label: "Исторический детектив", value: "istoricheskii-detektiv" },
{ label: "Героическая фантастика", value: "geroicheskaya-fantastika" }, { label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" },
{ label: "Героическое фэнтези", value: "geroicheskoe-fentezi" }, { label: "Историческое фэнтези", value: "istoricheskoe-fentezi" },
{ label: "Городское фэнтези", value: "gorodskoe-fentezi" }, { label: "Киберпанк", value: "kiberpank" },
{ label: "Детектив", value: "detektiv" }, { label: "Классический детектив", value: "klassicheskii-detektiv" },
{ label: "Детективная фантастика", value: "detektivnaya-fantastika" }, { label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" },
{ label: "Жёсткая эротика", value: "zhyostkaya-erotika" }, { label: "Космическая фантастика", value: "kosmicheskaya-fantastika" },
{ label: "Исторический детектив", value: "istoricheskii-detektiv" }, { label: "Криминальный детектив", value: "kriminalnyi-detektiv" },
{ label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" }, { label: "ЛитРПГ", value: "litrpg" },
{ label: "Историческое фэнтези", value: "istoricheskoe-fentezi" }, { label: "Любовная фантастика", value: "lyubovnaya-fantastika" },
{ label: "Киберпанк", value: "kiberpank" }, { label: "Любовное фэнтези", value: "lyubovnoe-fentezi" },
{ label: "Классический детектив", value: "klassicheskii-detektiv" }, { label: "Любовный роман", value: "lyubovnyi-roman" },
{ label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" }, { label: "Мистика", value: "mistika" },
{ label: "Космическая фантастика", value: "kosmicheskaya-fantastika" }, { label: "Молодежная проза", value: "molodezhnaya-proza" },
{ label: "Криминальный детектив", value: "kriminalnyi-detektiv" }, { label: "Научная фантастика", value: "nauchnaya-fantastika" },
{ label: "ЛитРПГ", value: "litrpg" }, { label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" },
{ label: "Любовная фантастика", value: "lyubovnaya-fantastika" }, { label: "Политический детектив", value: "politicheskii-detektiv" },
{ label: "Любовное фэнтези", value: "lyubovnoe-fentezi" }, { label: "Попаданцы", value: "popadantsy" },
{ label: "Любовный роман", value: "lyubovnyi-roman" }, { label: "Постапокалипсис", value: "postapokalipsis" },
{ label: "Мистика", value: "mistika" }, { label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" },
{ label: "Молодежная проза", value: "molodezhnaya-proza" }, { label: "Романтическая эротика", value: "romanticheskaya-erotika" },
{ label: "Научная фантастика", value: "nauchnaya-fantastika" }, { label: "С элементами эротики", value: "s-elementami-erotiki" },
{ label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" }, { label: "Славянское фэнтези", value: "slavyanskoe-fentezi" },
{ label: "Политический детектив", value: "politicheskii-detektiv" }, { label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" },
{ label: "Попаданцы", value: "popadantsy" }, { label: "Социальная фантастика", value: "sotsialnaya-fantastika" },
{ label: "Постапокалипсис", value: "postapokalipsis" }, { label: "Тёмное фэнтези", value: "temnoe-fentezi" },
{ label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" }, { label: "Фантастика", value: "fantastika" },
{ label: "Романтическая эротика", value: "romanticheskaya-erotika" }, { label: "Фэнтези", value: "fentezi" },
{ label: "С элементами эротики", value: "s-elementami-erotiki" }, { label: "Шпионский детектив", value: "shpionskii-detektiv" },
{ label: "Славянское фэнтези", value: "slavyanskoe-fentezi" }, { label: "Эпическое фэнтези", value: "epicheskoe-fentezi" },
{ label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" }, { label: "Эротика", value: "erotika" },
{ label: "Социальная фантастика", value: "sotsialnaya-fantastika" }, { label: "Эротическая фантастика", value: "eroticheskaya-fantastika" },
{ label: "Тёмное фэнтези", value: "temnoe-fentezi" }, { label: "Эротический фанфик", value: "eroticheskii-fanfik" },
{ label: "Фантастика", value: "fantastika" }, { label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" },
{ label: "Фэнтези", value: "fentezi" }, { label: "Юмористический детектив", value: "yumoristicheskii-detektiv" },
{ label: "Шпионский детектив", value: "shpionskii-detektiv" }, { label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" },
{ label: "Эпическое фэнтези", value: "epicheskoe-fentezi" }, ],
{ label: "Эротика", value: "erotika" }, type: FilterTypes.CheckboxGroup,
{ label: "Эротическая фантастика", value: "eroticheskaya-fantastika" }, },
{ label: "Эротический фанфик", value: "eroticheskii-fanfik" }, } satisfies Filters;
{ label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" }, }
{ label: "Юмористический детектив", value: "yumoristicheskii-detektiv" }, export default new Bookriver();
{ label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" },
], interface response {
type: FilterTypes.CheckboxGroup, props: {
}, pageProps: {
} satisfies Filters; state: {
} book?: {
export default new Bookriver(); bookPage: BooksEntity;
};
interface response { pagesFilter?: {
props: { genre: {
pageProps: { books: BooksEntity[];
state: { };
book?: { };
bookPage: BooksEntity; };
}; };
pagesFilter?: { };
genre: { }
books: BooksEntity[];
}; interface BooksEntity {
}; name: string;
}; coverImages: { url: string }[];
}; slug: string;
}; annotation?: string;
} author?: {
name: string;
interface BooksEntity { };
name: string; tags?: { name: string }[];
coverImages: { url: string }[]; ebook?: {
slug: string; chapters: Chapter[];
annotation?: string; };
author?: { statusComplete: string;
name: string; }
};
tags?: { name: string }[]; interface Chapter {
ebook?: { name: string;
chapters: Chapter[]; available: boolean;
}; firstPublishedAt?: Date | null;
statusComplete: string; createdAt?: Date | null;
} chapterId: number | string;
}
interface Chapter {
name: string; interface responseChapter {
available: boolean; data: {
firstPublishedAt?: Date | null; id: number;
createdAt?: Date | null; bookId: number;
chapterId: number | string; name: string;
} content: string;
symbols: number;
interface responseChapter { number: number;
data: { authorPages: number;
id: number; createdAt: string;
bookId: number; updatedAt: string;
name: string; firstPublishedAt: string;
content: string; status: string;
symbols: number; protected: boolean;
number: number; free: boolean;
authorPages: number; audio?: {
createdAt: string; available: boolean;
updatedAt: string; url: string;
firstPublishedAt: string; duration: number;
status: string; };
protected: boolean; publicationScheduledFor?: null;
free: boolean; };
audio?: { }
available: boolean;
url: string; interface responseSearch {
duration: number; data: Data;
}; total: number;
publicationScheduledFor?: null; }
}; interface Data {
} books?: BooksEntity[] | null;
}
interface responseSearch {
data: Data;
total: number;
}
interface Data {
books?: BooksEntity[] | null;
+2636 -2643
View File
File diff suppressed because it is too large Load Diff
+263 -264
View File
@@ -1,264 +1,263 @@
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
class Jaomix implements Plugin.PluginBase { class Jaomix implements Plugin.PluginBase {
id = "jaomix.ru"; id = "jaomix.ru";
name = "Jaomix"; name = "Jaomix";
site = "https://jaomix.ru"; site = "https://jaomix.ru";
version = "1.0.0"; version = "1.0.0";
icon = "src/ru/jaomix/icon.png"; icon = "src/ru/jaomix/icon.png";
async popularNovels(
pageNo: number,
async popularNovels( { showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
pageNo: number, ): Promise<Plugin.NovelItem[]> {
{ let url = this.site + "/?searchrn";
showLatestNovels,
filters, if (filters?.lang?.value?.length) {
}: Plugin.PopularNovelsOptions<typeof this.filters>, url += filters.lang.value
): Promise<Plugin.NovelItem[]> { .map((lang, idx) => `&lang[${idx}]=${lang}`)
let url = this.site + "/?searchrn"; .join("");
}
if (filters?.lang?.value?.length) {
url += filters.lang.value if (filters?.genre?.value?.include?.length) {
.map((lang, idx) => `&lang[${idx}]=${lang}`).join(""); url += filters.genre.value.include
} .map((genre, idx) => `&genre[${idx}]=${genre}`)
.join("");
if (filters?.genre?.value?.include?.length) { }
url += filters.genre.value.include
.map((genre, idx) => `&genre[${idx}]=${genre}`).join(""); if (filters?.genre?.value?.exclude?.length) {
} url += filters.genre.value.exclude
.map((genre, idx) => `&delgenre[${idx}]=del ${genre}`)
if (filters?.genre?.value?.exclude?.length) { .join("");
url += filters.genre.value.exclude }
.map((genre, idx) => `&delgenre[${idx}]=del ${genre}`).join("");
} url += "&sortcountchapt=" + (filters?.sortcountchapt?.value || "1");
url += "&sortdaycreate=" + (filters?.sortdaycreate?.value || "1");
url += "&sortcountchapt=" + (filters?.sortcountchapt?.value || "1"); url += "&sortby=" + (showLatestNovels ? "upd" : filters?.sortby?.value || "topweek");
url += "&sortdaycreate=" + (filters?.sortdaycreate?.value || "1"); url += "&gpage=" + pageNo;
url +=
"&sortby=" + (showLatestNovels ? "upd" : filters?.sortby?.value || "topweek"); const body = await fetchApi(url).then((res) => res.text());
url += "&gpage=" + pageNo; const loadedCheerio = parseHTML(body);
const body = await fetchApi(url).then((res) => res.text()); const novels: Plugin.NovelItem[] = [];
const loadedCheerio = parseHTML(body); loadedCheerio('div[class="block-home"] > div[class="one"]').each(
function () {
const novels: Plugin.NovelItem[] = []; const name = loadedCheerio(this)
loadedCheerio('div[class="block-home"] > div[class="one"]').each( .find('div[class="img-home"] > a')
function () { .attr("title");
const name = loadedCheerio(this) const cover = loadedCheerio(this)
.find('div[class="img-home"] > a') .find('div[class="img-home"] > a > img')
.attr("title"); .attr("src")
const cover = loadedCheerio(this) ?.replace("-150x150", "");
.find('div[class="img-home"] > a > img') const url = loadedCheerio(this)
.attr("src") .find('div[class="img-home"] > a')
?.replace("-150x150", ""); .attr("href");
const url = loadedCheerio(this)
.find('div[class="img-home"] > a') if (!name || !url) return;
.attr("href");
novels.push({ name, cover, url });
if (!name || !url) return; },
);
novels.push({ name, cover, url });
}, return novels;
); }
return novels; async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
} const body = await fetchApi(novelUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body);
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((res) => res.text()); const novel: Plugin.SourceNovel = {
const loadedCheerio = parseHTML(body); url: novelUrl,
name: loadedCheerio('div[class="desc-book"] > h1').text().trim(),
const novel: Plugin.SourceNovel = { cover: loadedCheerio('div[class="img-book"] > img').attr("src"),
url: novelUrl, summary: loadedCheerio('div[id="desc-tab"]').text().trim(),
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() loadedCheerio("#info-book > p").each(function () {
}; let text = loadedCheerio(this).text().replace(/,/g, "").split(" ");
if (text[0] === "Автор:") {
loadedCheerio("#info-book > p").each(function () { novel.author = text.splice(1).join(" ");
let text = loadedCheerio(this).text().replace(/,/g, "").split(" "); } else if (text[0] === "Жанры:") {
if (text[0] === "Автор:") { novel.genres = text.splice(1).join(",");
novel.author = text.splice(1).join(" "); } else if (text[0] === "Статус:") {
} else if (text[0] === "Жанры:") { novel.status = text.includes("продолжается")
novel.genres = text.splice(1).join(","); ? NovelStatus.Ongoing
} else if (text[0] === "Статус:") { : NovelStatus.Completed;
novel.status = text.includes("продолжается") }
? NovelStatus.Ongoing });
: NovelStatus.Completed;
} const chapters: Plugin.ChapterItem[] = [];
}); const totalChapters = loadedCheerio(".download-chapter div.title").length;
const chapters: Plugin.ChapterItem[] = []; loadedCheerio(".download-chapter div.title").each((chapterIndex, element) => {
const name = loadedCheerio(element).find("a").attr("title");
loadedCheerio(".download-chapter div.title").each(function () { const url = loadedCheerio(element).find("a").attr("href");
const name = loadedCheerio(this).find("a").attr("title"); if (!name || !url) return;
const releaseTime = loadedCheerio(this).find("time").text();
const url = loadedCheerio(this).find("a").attr("href"); const releaseTime = loadedCheerio(element).find("time").text();
if (!name || !url) return; chapters.push({ name, url, releaseTime, chapterNumber: totalChapters - chapterIndex });
});
chapters.push({ name, releaseTime, url });
}); novel.chapters = chapters.reverse();
return novel;
novel.chapters = chapters.reverse(); }
return novel;
} async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
async parseChapter(chapterUrl: string): Promise<string> { const loadedCheerio = parseHTML(body);
const body = await fetchApi(chapterUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body); loadedCheerio('div[class="adblock-service"]').remove();
const chapterText = loadedCheerio('div[class="entry-content"]').html();
loadedCheerio('div[class="adblock-service"]').remove();
const chapterText = loadedCheerio('div[class="entry-content"]').html(); return chapterText || "";
}
return chapterText || "";
} async searchNovels(
searchTerm: string,
async searchNovels( pageNo: number | undefined = 1,
searchTerm: string, ): Promise<Plugin.NovelItem[]> {
pageNo: number | undefined = 1, const url = `${this.site}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd&gpage=${pageNo}`;
): Promise<Plugin.NovelItem[]> { const body = await fetchApi(url).then((res) => res.text());
const url = `${this.site}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd&gpage=${pageNo}`; const loadedCheerio = parseHTML(body);
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(
const novels: Plugin.NovelItem[] = []; function () {
loadedCheerio('div[class="block-home"] > div[class="one"]').each( const name = loadedCheerio(this)
function () { .find('div[class="img-home"] > a')
const name = loadedCheerio(this) .attr("title");
.find('div[class="img-home"] > a') const cover = loadedCheerio(this)
.attr("title"); .find('div[class="img-home"] > a > img')
const cover = loadedCheerio(this) .attr("src")
.find('div[class="img-home"] > a > img') ?.replace("-150x150", "");
.attr("src") const url = loadedCheerio(this)
?.replace("-150x150", ""); .find('div[class="img-home"] > a')
const url = loadedCheerio(this) .attr("href");
.find('div[class="img-home"] > a')
.attr("href"); if (!name || !url) return;
if (!name || !url) return; novels.push({ name, cover, url });
},
novels.push({ name, cover, url }); );
},
); return novels;
}
return novels; fetchImage = fetchFile;
}
fetchImage = fetchFile; filters = {
sortby: {
filters = { label: "Сортировка:",
sortby: { value: "topweek",
label: "Сортировка:", options: [
value: "topweek", { label: "Топ недели", value: "topweek" },
options: [ { label: "По алфавиту", value: "alphabet" },
{ label: "Топ недели", value: "topweek" }, { label: "По дате обновления", value: "upd" },
{ label: "По алфавиту", value: "alphabet" }, { label: "По дате создания", value: "new" },
{ label: "По дате обновления", value: "upd" }, { label: "По просмотрам", value: "count" },
{ label: "По дате создания", value: "new" }, { label: "Топ года", value: "topyear" },
{ label: "По просмотрам", value: "count" }, { label: "Топ дня", value: "topday" },
{ label: "Топ года", value: "topyear" }, { label: "Топ за все время", value: "alltime" },
{ label: "Топ дня", value: "topday" }, { label: "Топ месяца", value: "topmonth" },
{ label: "Топ за все время", value: "alltime" }, ],
{ label: "Топ месяца", value: "topmonth" }, type: FilterTypes.Picker,
], },
type: FilterTypes.Picker, sortdaycreate: {
}, label: "Дата добавления:",
sortdaycreate: { value: "1",
label: "Дата добавления:", options: [
value: "1", { label: "Любое", value: "1" },
options: [ { label: "От 120 до 180 дней", value: "1218" },
{ label: "Любое", value: "1" }, { label: "От 180 до 365 дней", value: "1836" },
{ label: "От 120 до 180 дней", value: "1218" }, { label: "От 30 до 60 дней", value: "3060" },
{ label: "От 180 до 365 дней", value: "1836" }, { label: "От 365 дней", value: "365" },
{ label: "От 30 до 60 дней", value: "3060" }, { label: "От 60 до 90 дней", value: "6090" },
{ label: "От 365 дней", value: "365" }, { label: "От 90 до 120 дней", value: "9012" },
{ label: "От 60 до 90 дней", value: "6090" }, { label: "Послед. 30 дней", value: "30" },
{ label: "От 90 до 120 дней", value: "9012" }, ],
{ label: "Послед. 30 дней", value: "30" }, type: FilterTypes.Picker,
], },
type: FilterTypes.Picker, sortcountchapt: {
}, label: "Количество глав:",
sortcountchapt: { value: "1",
label: "Количество глав:", options: [
value: "1", { label: "Любое кол-во глав", value: "1" },
options: [ { label: "До 500", value: "500" },
{ label: "Любое кол-во глав", value: "1" }, { label: "От 1000 до 2000", value: "1020" },
{ label: "До 500", value: "500" }, { label: "От 2000 до 3000", value: "2030" },
{ label: "От 1000 до 2000", value: "1020" }, { label: "От 3000 до 4000", value: "3040" },
{ label: "От 2000 до 3000", value: "2030" }, { label: "От 4000", value: "400" },
{ label: "От 3000 до 4000", value: "3040" }, { label: "От 500 до 1000", value: "510" },
{ label: "От 4000", value: "400" }, ],
{ label: "От 500 до 1000", value: "510" }, type: FilterTypes.Picker,
], },
type: FilterTypes.Picker, genre: {
}, label: "Жанры:",
genre: { value: { include: [], exclude: [] },
label: "Жанры:", options: [
value: { include: [], exclude: [] }, { label: "Боевые Искусства", value: "Боевые Искусства" },
options: [ { label: "Виртуальный Мир", value: "Виртуальный Мир" },
{ label: "Боевые Искусства", value: "Боевые Искусства" }, { label: "Гарем", value: "Гарем" },
{ label: "Виртуальный Мир", value: "Виртуальный Мир" }, { label: "Детектив", value: "Детектив" },
{ label: "Гарем", value: "Гарем" }, { label: "Драма", value: "Драма" },
{ label: "Детектив", value: "Детектив" }, { label: "Игра", value: "Игра" },
{ label: "Драма", value: "Драма" }, { label: "Истории из жизни", value: "Истории из жизни" },
{ label: "Игра", value: "Игра" }, { label: "Исторический", value: "Исторический" },
{ label: "Истории из жизни", value: "Истории из жизни" }, { label: "История", value: "История" },
{ label: "Исторический", value: "Исторический" }, { label: "Исэкай", value: "Исэкай" },
{ label: "История", value: "История" }, { label: "Комедия", value: "Комедия" },
{ label: "Исэкай", value: "Исэкай" }, { label: "Меха", value: "Меха" },
{ label: "Комедия", value: "Комедия" }, { label: "Мистика", value: "Мистика" },
{ label: "Меха", value: "Меха" }, { label: "Научная Фантастика", value: "Научная Фантастика" },
{ label: "Мистика", value: "Мистика" }, { label: "Повседневность", value: "Повседневность" },
{ label: "Научная Фантастика", value: "Научная Фантастика" }, { label: "Постапокалипсис", value: "Постапокалипсис" },
{ label: "Повседневность", value: "Повседневность" }, { label: "Приключения", value: "Приключения" },
{ label: "Постапокалипсис", value: "Постапокалипсис" }, { label: "Психология", value: "Психология" },
{ label: "Приключения", value: "Приключения" }, { label: "Романтика", value: "Романтика" },
{ label: "Психология", value: "Психология" }, { label: "Сверхъестественное", value: "Сверхъестественное" },
{ label: "Романтика", value: "Романтика" }, { label: "Сёнэн", value: "Сёнэн" },
{ label: "Сверхъестественное", value: "Сверхъестественное" }, { label: "Сёнэн-ай", value: "Сёнэн-ай" },
{ label: "Сёнэн", value: "Сёнэн" }, { label: "Спорт", value: "Спорт" },
{ label: "Сёнэн-ай", value: "Сёнэн-ай" }, { label: "Сэйнэн", value: "Сэйнэн" },
{ label: "Спорт", value: "Спорт" }, { label: "Сюаньхуа", value: "Сюаньхуа" },
{ label: "Сэйнэн", value: "Сэйнэн" }, { label: "Трагедия", value: "Трагедия" },
{ label: "Сюаньхуа", value: "Сюаньхуа" }, { label: "Триллер", value: "Триллер" },
{ label: "Трагедия", value: "Трагедия" }, { label: "Фантастика", value: "Фантастика" },
{ label: "Триллер", value: "Триллер" }, { label: "Фэнтези", value: "Фэнтези" },
{ label: "Фантастика", value: "Фантастика" }, { label: "Хоррор", value: "Хоррор" },
{ label: "Фэнтези", value: "Фэнтези" }, { label: "Школьная жизнь", value: "Школьная жизнь" },
{ label: "Хоррор", value: "Хоррор" }, { label: "Шоунен", value: "Шоунен" },
{ label: "Школьная жизнь", value: "Школьная жизнь" }, { label: "Экшн", value: "Экшн" },
{ label: "Шоунен", value: "Шоунен" }, { label: "Этти", value: "Этти" },
{ label: "Экшн", value: "Экшн" }, { label: "Юри", value: "Юри" },
{ label: "Этти", value: "Этти" }, { label: "Adult", value: "Adult" },
{ label: "Юри", value: "Юри" }, { label: "Ecchi", value: "Ecchi" },
{ label: "Adult", value: "Adult" }, { label: "Josei", value: "Josei" },
{ label: "Ecchi", value: "Ecchi" }, { label: "Lolicon", value: "Lolicon" },
{ label: "Josei", value: "Josei" }, { label: "Mature", value: "Mature" },
{ label: "Lolicon", value: "Lolicon" }, { label: "Shoujo", value: "Shoujo" },
{ label: "Mature", value: "Mature" }, { label: "Wuxia", value: "Wuxia" },
{ label: "Shoujo", value: "Shoujo" }, { label: "Xianxia", value: "Xianxia" },
{ label: "Wuxia", value: "Wuxia" }, { label: "Xuanhuan", value: "Xuanhuan" },
{ label: "Xianxia", value: "Xianxia" }, { label: "Yaoi", value: "Yaoi" },
{ label: "Xuanhuan", value: "Xuanhuan" }, ],
{ label: "Yaoi", value: "Yaoi" }, type: FilterTypes.ExcludableCheckboxGroup,
], },
type: FilterTypes.ExcludableCheckboxGroup, lang: {
}, label: "Выбрать языки:",
lang: { value: [],
label: "Выбрать языки:", options: [
value: [], { label: "Английский", value: "Английский" },
options: [ { label: "Китайский", value: "Китайский" },
{ label: "Английский", value: "Английский" }, { label: "Корейский", value: "Корейский" },
{ label: "Китайский", value: "Китайский" }, { label: "Японский", value: "Японский" },
{ label: "Корейский", value: "Корейский" }, ],
{ label: "Японский", value: "Японский" }, type: FilterTypes.CheckboxGroup,
], },
type: FilterTypes.CheckboxGroup, } satisfies Filters;
}, }
} satisfies Filters;
} export default new Jaomix();
+963 -957
View File
File diff suppressed because it is too large Load Diff
+1269 -1284
View File
File diff suppressed because it is too large Load Diff
+484 -512
View File
@@ -1,512 +1,484 @@
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
import dayjs from "dayjs"; import dayjs from "dayjs";
class RLIB implements Plugin.PluginBase { const statusKey = [
id = "RLIB"; NovelStatus.Ongoing,
name = "RanobeLib"; NovelStatus.Completed,
site = "https://ranobelib.me"; NovelStatus.OnHiatus,
version = "1.0.0"; NovelStatus.Cancelled,
icon = "src/ru/ranobelib/icon.png"; ];
ui: string | number | undefined = undefined;
class RLIB implements Plugin.PluginBase {
id = "RLIB";
name = "RanobeLib";
async popularNovels( site = "https://ranobelib.me";
pageNo: number, version = "1.0.0";
{ icon = "src/ru/ranobelib/icon.png";
showLatestNovels, ui: string | undefined = undefined;
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>, async popularNovels(
): Promise<Plugin.NovelItem[]> { pageNo: number,
let url = `${this.site}/manga-list?sort=`; { showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
url += showLatestNovels ): Promise<Plugin.NovelItem[]> {
? "last_chapter_at" let url = this.site + "/manga-list?sort=";
: filters?.sort?.value || "rate"; url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate";
url += "&dir=" + (filters?.order?.value || "desc"); url += "&dir=" + (filters?.order?.value || "desc");
if (filters.type?.value?.length) { Object.entries(filters || {}).forEach(([type, { value }]: any) => {
url += filters.type.value.map((i) => "&types[]=" + i).join(""); if (value instanceof Array && value.length) {
} url += "&" + type + "[]=" + value.join("&" + type + "[]=");
}
if (filters.format?.value?.include?.length) { if (value?.include instanceof Array && value.include.length) {
url += filters.format.value.include url += "&" + type + "[include][]=" +
.map((i) => "&format[include][]=" + i).join(""); value.include.join("&" + type + "[include][]=");
} }
if (value?.exclude instanceof Array && value.exclude.length) {
if (filters.format?.value?.exclude?.length) { url += "&" + type + "[exclude][]=" +
url += filters.format.value.exclude value.exclude.join("&" + type + "[exclude][]=");
.map((i) => "&format[exclude][]=" + i).join(""); }
} });
if (filters.status?.value?.length) { url += "&page=" + pageNo;
url += filters.status.value.map((i) => "&status[]=" + i).join("");
} const result = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(result);
if (filters.statuss?.value?.length) { this.ui = loadedCheerio("a.header-right-menu__item")
url += filters.statuss.value.map((i) => "&manga_status[]=" + i).join(""); .attr("href")
} ?.replace?.(/[^0-9]/g, "");
if (filters.genres?.value?.include?.length) { const novels: Plugin.NovelItem[] = [];
url += filters.genres.value.include loadedCheerio(".media-card-wrap").each(function () {
.map((i) => "&genres[include][]=" + i).join(""); 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");
if (filters.genres?.value?.exclude?.length) { if (!url) return;
url += filters.genres.value.exclude novels.push({ name, cover, url });
.map((i) => "&genres[exclude][]=" + i).join(""); });
}
return novels;
if (filters.tags?.value?.include?.length) { }
url += filters.tags.value.include
.map((i) => "&tags[include][]=" + i).join(""); async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
} const body = await fetchApi(novelUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body);
if (filters.tags?.value?.exclude?.length) {
url += filters.tags.value.exclude const novel: Plugin.SourceNovel = {
.map((i) => "&tags[exclude][]=" + i).join(""); url: novelUrl,
} };
url += "&page=" + pageNo; novel.name = loadedCheerio(".media-name__main").text().trim();
const result = await fetchApi(url); novel.cover = loadedCheerio(".container_responsive img").attr("src");
const body = await result.text();
novel.summary = loadedCheerio(".media-description__text").text().trim();
const loadedCheerio = parseHTML(body);
this.ui = loadedCheerio("a.header-right-menu__item") novel.genres = loadedCheerio('div[class="media-tags"]')
.attr("href") .text()
?.replace?.(/[^0-9]/g, ""); .trim()
.replace(/[\n\r]+/g, ", ")
const novels: Plugin.NovelItem[] = []; .replace(/ /g, "");
loadedCheerio(".media-card-wrap").each(function () {
const name = loadedCheerio(this).find(".media-card__title").text(); loadedCheerio(
const cover = loadedCheerio(this).find("a.media-card").attr("data-src"); 'div[class="media-info-list paper"] > [class="media-info-list__item"]',
const url = loadedCheerio(this).find("a.media-card").attr("href"); ).each(function () {
if (!url) return; let name = loadedCheerio(this)
novels.push({ name, cover, url }); .find('div[class="media-info-list__title"]')
}); .text();
return novels; if (name === "Автор") {
} novel.author = loadedCheerio(this)
.find('div[class="media-info-list__value"]')
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> { .text()
const result = await fetchApi(novelUrl); .trim();
const body = await result.text(); } else if (name === "Художник") {
novel.artist = loadedCheerio(this)
const loadedCheerio = parseHTML(body); .find('div[class="media-info-list__value"]')
.text()
const novel: Plugin.SourceNovel = { .trim();
url: novelUrl, }
}; });
novel.name = loadedCheerio(".media-name__main").text().trim(); const chapters: Plugin.ChapterItem[] = [];
novel.cover = loadedCheerio(".media-sidebar__cover img").attr("src"); let chaptersRaw: any = body.match(
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm,
novel.summary = loadedCheerio(".media-description__text").text().trim(); );
novel.genres = loadedCheerio('div[class="media-tags"]') chaptersRaw = chaptersRaw?.[0]
.text() ?.replace("window.__DATA__ = ", "")
.trim() ?.replace("window._SITE_COLOR_", "")
.replace(/[\n\r]+/g, ", ") ?.trim()
.replace(/ /g, ""); ?.slice(0, -1);
loadedCheerio( const chaptersJson: responseBook = JSON.parse(chaptersRaw);
'div[class="media-info-list paper"] > [class="media-info-list__item"]', const totalChapters = chaptersJson.chapters.list?.length || 0;
).each(function () { novel.status = statusKey[chaptersJson.manga.status - 1] || NovelStatus.Unknown;
let name = loadedCheerio(this) this.ui = chaptersJson?.user?.id;
.find('div[class="media-info-list__title"]')
.text(); chaptersJson.chapters?.list?.forEach((chapter, chapterIndex) =>
chapters.push({
if (name === "Статус перевода") { name:
novel.status = "Том " + chapter.chapter_volume +
loadedCheerio(this).find("div:nth-child(2)").text().trim() === " Глава " + chapter.chapter_number +
"Продолжается" (chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
? NovelStatus.Ongoing url:
: NovelStatus.Completed; this.site + "/" + chaptersJson.manga.slug +
} else if (name === "Автор") { "/v" + chapter.chapter_volume + "/c" + chapter.chapter_number +
novel.author = loadedCheerio(this) "?bid=" + (chapter.branch_id || ""),
.find("div:nth-child(2)") releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
.text() chapterNumber: totalChapters - chapterIndex,
.trim(); }),
} );
});
novel.chapters = chapters.reverse();
const chapters: Plugin.ChapterItem[] = []; return novel;
}
let chaptersRaw: any = body.match(
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm, async parseChapter(chapterUrl: string): Promise<string> {
); const result = await fetchApi(
chapterUrl + (this.ui ? "&ui=" + this.ui : ""),
chaptersRaw = chaptersRaw?.[0] ).then((res) => res.text());
?.replace("window.__DATA__ = ", "")
?.replace("window._SITE_COLOR_", "") const loadedCheerio = parseHTML(result);
?.trim() loadedCheerio(".reader-container img").each((index, element) => {
?.slice(0, -1); const src =
loadedCheerio(element).attr("data-src") ||
let chaptersJson: responseBook = JSON.parse(chaptersRaw); loadedCheerio(element).attr("src");
this.ui = chaptersJson?.user?.id; if (!src?.startsWith("http")) {
loadedCheerio(element).attr("src", this.site + src);
chaptersJson.chapters?.list?.forEach((chapter) => } else {
chapters.push({ loadedCheerio(element).attr("src", src);
name: `Том ${chapter.chapter_volume} Глава ${chapter.chapter_number} ${chapter.chapter_name}`?.trim(), }
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"), loadedCheerio(element).removeAttr("data-src");
url: });
`${this.site}/${chaptersJson.manga.slug}/v${chapter.chapter_volume}/c${chapter.chapter_number}?bid=` +
(chapter?.branch_id || ""), const chapterText = loadedCheerio(".reader-container").html();
}), return chapterText || "";
); }
novel.chapters = chapters.reverse(); async searchNovels(
return novel; searchTerm: string,
} //pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
async parseChapter(chapterUrl: string): Promise<string> { const result = await fetchApi(
const result = await fetchApi( `${this.site}/search?q=${searchTerm}&type=manga`,
chapterUrl + (this.ui ? "&ui=" + this.ui : ""), );
); const body = (await result.json()) as Manga[];
const body = await result.text(); const novels: Plugin.NovelItem[] = [];
const loadedCheerio = parseHTML(body); body.forEach((novel) =>
novels.push({
const baseUrl = this.site; name: novel?.rus_name || novel.name,
loadedCheerio(".reader-container img").each(function () { cover: novel?.coverImage,
const src = url: novel?.href || this.site + "/" + novel.slug,
loadedCheerio(this).attr("data-src") || loadedCheerio(this).attr("src"); }),
if (!src?.startsWith("http")) { );
loadedCheerio(this).attr("src", baseUrl + src);
} else { return novels;
loadedCheerio(this).attr("src", src); }
}
loadedCheerio(this).removeAttr("data-src"); fetchImage = fetchFile;
});
filters = {
const chapterText = loadedCheerio(".reader-container").html(); sort: {
label: "Сортировка",
return chapterText || ""; value: "rate",
} options: [
{ label: "Рейтинг", value: "rate" },
async searchNovels( { label: "Имя", value: "name" },
searchTerm: string, { label: "Просмотры", value: "views" },
//pageNo: number | undefined = 1, { label: "Дате добавления", value: "created_at" },
): Promise<Plugin.NovelItem[]> { { label: "Дате обновления", value: "last_chapter_at" },
const result = await fetchApi( { label: "Количество глав", value: "chap_count" },
`${this.site}/search?q=${searchTerm}&type=manga`, ],
); type: FilterTypes.Picker,
const body = (await result.json()) as Manga[]; },
const novels: Plugin.NovelItem[] = []; order: {
label: "Порядок",
body.forEach((novel) => value: "desc",
novels.push({ options: [
name: novel?.rus_name || novel.name, { label: "По убыванию", value: "desc" },
cover: novel?.coverImage, { label: "По возрастанию", value: "asc" },
url: novel?.href || this.site + "/" + novel.slug, ],
}), type: FilterTypes.Picker,
); },
type: {
return novels; label: "Тип",
} value: [],
options: [
fetchImage = fetchFile; { label: "Авторский", value: "14" },
{ label: "Английский", value: "13" },
filters = { { label: "Китай", value: "12" },
sort: { { label: "Корея", value: "11" },
label: "Сортировка", { label: "Фанфик", value: "15" },
value: "", { label: "Япония", value: "10" },
options: [ ],
{ label: "Рейтинг", value: "rate" }, type: FilterTypes.CheckboxGroup,
{ label: "Имя", value: "name" }, },
{ label: "Просмотры", value: "views" }, format: {
{ label: "Дате добавления", value: "created_at" }, label: "Формат выпуска",
{ label: "Дате обновления", value: "last_chapter_at" }, value: { include: [], exclude: [] },
{ label: "Количество глав", value: "chap_count" }, options: [
], { label: "4-кома (Ёнкома)", value: "1" },
type: FilterTypes.Picker, { label: "В цвете", value: "4" },
}, { label: "Веб", value: "6" },
order: { { label: "Вебтун", value: "7" },
label: "Порядок", { label: "Додзинси", value: "3" },
value: "", { label: "Сборник", value: "2" },
options: [ { label: "Сингл", value: "5" },
{ label: "По убыванию", value: "desc" }, ],
{ label: "По возрастанию", value: "asc" }, type: FilterTypes.ExcludableCheckboxGroup,
], },
type: FilterTypes.Picker, status: {
}, label: "Статус перевода",
type: { value: [],
label: "Тип", options: [
value: [], { label: "Продолжается", value: "1" },
options: [ { label: "Завершен", value: "2" },
{ label: "Авторский", value: "14" }, { label: "Заморожен", value: "3" },
{ label: "Английский", value: "13" }, { label: "Заброшен", value: "4" },
{ label: "Китай", value: "12" }, ],
{ label: "Корея", value: "11" }, type: FilterTypes.CheckboxGroup,
{ label: "Фанфик", value: "15" }, },
{ label: "Япония", value: "10" }, manga_status: {
], label: "Статус тайтла",
type: FilterTypes.CheckboxGroup, value: [],
}, options: [
format: { { label: "Онгоинг", value: "1" },
label: "Формат выпуска", { label: "Завершён", value: "2" },
value: { include: [], exclude: [] }, { label: "Анонс", value: "3" },
options: [ { label: "Приостановлен", value: "4" },
{ label: "4-кома (Ёнкома)", value: "1" }, { label: "Выпуск прекращён", value: "5" },
{ label: "В цвете", value: "4" }, ],
{ label: "Веб", value: "6" }, type: FilterTypes.CheckboxGroup,
{ label: "Вебтун", value: "7" }, },
{ label: "Додзинси", value: "3" }, genres: {
{ label: "Сборник", value: "2" }, label: "Жанры",
{ label: "Сингл", value: "5" }, value: { include: [], exclude: [] },
], options: [
type: FilterTypes.ExcludableCheckboxGroup, { label: "Арт", value: "32" },
}, { label: "Безумие", value: "91" },
status: { { label: "Боевик", value: "34" },
label: "Статус перевода", { label: "Боевые искусства", value: "35" },
value: [], { label: "Вампиры", value: "36" },
options: [ { label: "Военное", value: "89" },
{ label: "Продолжается", value: "1" }, { label: "Гарем", value: "37" },
{ label: "Завершен", value: "2" }, { label: "Гендерная интрига", value: "38" },
{ label: "Заморожен", value: "3" }, { label: "Героическое фэнтези", value: "39" },
{ label: "Заброшен", value: "4" }, { label: "Демоны", value: "81" },
], { label: "Детектив", value: "40" },
type: FilterTypes.CheckboxGroup, { label: "Детское", value: "88" },
}, { label: "Дзёсэй", value: "41" },
statuss: { { label: "Драма", value: "43" },
label: "Статус тайтла", { label: "Игра", value: "44" },
value: [], { label: "Исекай", value: "79" },
options: [ { label: "История", value: "45" },
{ label: "Онгоинг", value: "1" }, { label: "Киберпанк", value: "46" },
{ label: "Завершён", value: "2" }, { label: "Кодомо", value: "76" },
{ label: "Анонс", value: "3" }, { label: "Комедия", value: "47" },
{ label: "Приостановлен", value: "4" }, { label: "Космос", value: "83" },
{ label: "Выпуск прекращён", value: "5" }, { label: "Магия", value: "85" },
], { label: "Махо-сёдзё", value: "48" },
type: FilterTypes.CheckboxGroup, { label: "Машины", value: "90" },
}, { label: "Меха", value: "49" },
genres: { { label: "Мистика", value: "50" },
label: "Жанры", { label: "Музыка", value: "80" },
value: { include: [], exclude: [] }, { label: "Научная фантастика", value: "51" },
options: [ { label: "Омегаверс", value: "77" },
{ label: "Арт", value: "32" }, { label: "Пародия", value: "86" },
{ label: "Безумие", value: "91" }, { label: "Повседневность", value: "52" },
{ label: "Боевик", value: "34" }, { label: "Полиция", value: "82" },
{ label: "Боевые искусства", value: "35" }, { label: "Постапокалиптика", value: "53" },
{ label: "Вампиры", value: "36" }, { label: "Приключения", value: "54" },
{ label: "Военное", value: "89" }, { label: "Психология", value: "55" },
{ label: "Гарем", value: "37" }, { label: "Романтика", value: "56" },
{ label: "Гендерная интрига", value: "38" }, { label: "Самурайский боевик", value: "57" },
{ label: "Героическое фэнтези", value: "39" }, { label: "Сверхъестественное", value: "58" },
{ label: "Демоны", value: "81" }, { label: "Сёдзё", value: "59" },
{ label: "Детектив", value: "40" }, { label: "Сёдзё-ай", value: "60" },
{ label: "Детское", value: "88" }, { label: "Сёнэн", value: "61" },
{ label: "Дзёсэй", value: "41" }, { label: "Сёнэн-ай", value: "62" },
{ label: "Драма", value: "43" }, { label: "Спорт", value: "63" },
{ label: "Игра", value: "44" }, { label: "Супер сила", value: "87" },
{ label: "Исекай", value: "79" }, { label: "Сэйнэн", value: "64" },
{ label: "История", value: "45" }, { label: "Трагедия", value: "65" },
{ label: "Киберпанк", value: "46" }, { label: "Триллер", value: "66" },
{ label: "Кодомо", value: "76" }, { label: "Ужасы", value: "67" },
{ label: "Комедия", value: "47" }, { label: "Фантастика", value: "68" },
{ label: "Космос", value: "83" }, { label: "Фэнтези", value: "69" },
{ label: "Магия", value: "85" }, { label: "Школа", value: "70" },
{ label: "Махо-сёдзё", value: "48" }, { label: "Эротика", value: "71" },
{ label: "Машины", value: "90" }, { label: "Этти", value: "72" },
{ label: "Меха", value: "49" }, { label: "Юри", value: "73" },
{ label: "Мистика", value: "50" }, { label: "Яой", value: "74" },
{ label: "Музыка", value: "80" }, ],
{ label: "Научная фантастика", value: "51" }, type: FilterTypes.ExcludableCheckboxGroup,
{ label: "Омегаверс", value: "77" }, },
{ label: "Пародия", value: "86" }, tags: {
{ label: "Повседневность", value: "52" }, label: "Теги",
{ label: "Полиция", value: "82" }, value: { include: [], exclude: [] },
{ label: "Постапокалиптика", value: "53" }, options: [
{ label: "Приключения", value: "54" }, { label: "Авантюристы", value: "328" },
{ label: "Психология", value: "55" }, { label: "Антигерой", value: "176" },
{ label: "Романтика", value: "56" }, { label: "Бессмертные", value: "333" },
{ label: "Самурайский боевик", value: "57" }, { label: "Боги", value: "218" },
{ label: "Сверхъестественное", value: "58" }, { label: "Борьба за власть", value: "309" },
{ label: "Сёдзё", value: "59" }, { label: "Брат и сестра", value: "360" },
{ label: "Сёдзё-ай", value: "60" }, { label: "Ведьма", value: "339" },
{ label: "Сёнэн", value: "61" }, { label: "Видеоигры", value: "204" },
{ label: "Сёнэн-ай", value: "62" }, { label: "Виртуальная реальность", value: "214" },
{ label: "Спорт", value: "63" }, { label: "Владыка демонов", value: "349" },
{ label: "Супер сила", value: "87" }, { label: "Военные", value: "198" },
{ label: "Сэйнэн", value: "64" }, { label: "Воспоминания из другого мира", value: "310" },
{ label: "Трагедия", value: "65" }, { label: "Выживание", value: "212" },
{ label: "Триллер", value: "66" }, { label: "ГГ женщина", value: "294" },
{ label: "Ужасы", value: "67" }, { label: "ГГ имба", value: "292" },
{ label: "Фантастика", value: "68" }, { label: "ГГ мужчина", value: "295" },
{ label: "Фэнтези", value: "69" }, { label: "ГГ не ояш", value: "325" },
{ label: "Школа", value: "70" }, { label: "ГГ не человек", value: "331" },
{ label: "Эротика", value: "71" }, { label: "ГГ ояш", value: "326" },
{ label: "Этти", value: "72" }, { label: "Главный герой бог", value: "324" },
{ label: "Юри", value: "73" }, { label: "Глупый ГГ", value: "298" },
{ label: "Яой", value: "74" }, { label: "Горничные", value: "171" },
], { label: "Гуро", value: "306" },
type: FilterTypes.ExcludableCheckboxGroup, { label: "Гяру", value: "197" },
}, { label: "Демоны", value: "157" },
tags: { { label: "Драконы", value: "313" },
label: "Теги", { label: "Древний мир", value: "317" },
value: { include: [], exclude: [] }, { label: "Зверолюди", value: "163" },
options: [ { label: "Зомби", value: "155" },
{ label: "Авантюристы", value: "328" }, { label: "Исторические фигуры", value: "323" },
{ label: "Антигерой", value: "176" }, { label: "Кулинария", value: "158" },
{ label: "Бессмертные", value: "333" }, { label: "Культивация", value: "161" },
{ label: "Боги", value: "218" }, { label: "ЛГБТ", value: "344" },
{ label: "Борьба за власть", value: "309" }, { label: "ЛитРПГ", value: "319" },
{ label: "Брат и сестра", value: "360" }, { label: "Лоли", value: "206" },
{ label: "Ведьма", value: "339" }, { label: "Магия", value: "170" },
{ label: "Видеоигры", value: "204" }, { label: "Машинный перевод", value: "345" },
{ label: "Виртуальная реальность", value: "214" }, { label: "Медицина", value: "159" },
{ label: "Владыка демонов", value: "349" }, { label: "Межгалактическая война", value: "330" },
{ label: "Военные", value: "198" }, { label: "Монстр Девушки", value: "207" },
{ label: "Воспоминания из другого мира", value: "310" }, { label: "Монстры", value: "208" },
{ label: "Выживание", value: "212" }, { label: "Мрачный мир", value: "316" },
{ label: "ГГ женщина", value: "294" }, { label: "Музыка", value: "358" },
{ label: "ГГ имба", value: "292" }, { label: "Музыка", value: "209" },
{ label: "ГГ мужчина", value: "295" }, { label: "Ниндзя", value: "199" },
{ label: "ГГ не ояш", value: "325" }, { label: "Обратный Гарем", value: "210" },
{ label: "ГГ не человек", value: "331" }, { label: "Офисные Работники", value: "200" },
{ label: "ГГ ояш", value: "326" }, { label: "Пираты", value: "341" },
{ label: "Главный герой бог", value: "324" }, { label: "Подземелья", value: "314" },
{ label: "Глупый ГГ", value: "298" }, { label: "Политика", value: "311" },
{ label: "Горничные", value: "171" }, { label: "Полиция", value: "201" },
{ label: "Гуро", value: "306" }, { label: "Преступники / Криминал", value: "205" },
{ label: "Гяру", value: "197" }, { label: "Призраки / Духи", value: "196" },
{ label: "Демоны", value: "157" }, { label: "Призыватели", value: "329" },
{ label: "Драконы", value: "313" }, { label: "Прыжки между мирами", value: "321" },
{ label: "Древний мир", value: "317" }, { label: "Путешествие в другой мир", value: "318" },
{ label: "Зверолюди", value: "163" }, { label: "Путешествие во времени", value: "213" },
{ label: "Зомби", value: "155" }, { label: "Рабы", value: "355" },
{ label: "Исторические фигуры", value: "323" }, { label: "Ранги силы", value: "312" },
{ label: "Кулинария", value: "158" }, { label: "Реинкарнация", value: "154" },
{ label: "Культивация", value: "161" }, { label: "Самураи", value: "202" },
{ label: "ЛГБТ", value: "344" }, { label: "Скрытие личности", value: "315" },
{ label: "ЛитРПГ", value: "319" }, { label: "Средневековье", value: "174" },
{ label: "Лоли", value: "206" }, { label: "Традиционные игры", value: "203" },
{ label: "Магия", value: "170" }, { label: "Умный ГГ", value: "303" },
{ label: "Машинный перевод", value: "345" }, { label: "Характерный рост", value: "332" },
{ label: "Медицина", value: "159" }, { label: "Хикикомори", value: "167" },
{ label: "Межгалактическая война", value: "330" }, { label: "Эволюция", value: "322" },
{ label: "Монстр Девушки", value: "207" }, { label: "Элементы РПГ", value: "327" },
{ label: "Монстры", value: "208" }, { label: "Эльфы", value: "217" },
{ label: "Мрачный мир", value: "316" }, { label: "Якудза", value: "165" },
{ label: "Музыка", value: "358" }, ],
{ label: "Музыка", value: "209" }, type: FilterTypes.ExcludableCheckboxGroup,
{ label: "Ниндзя", value: "199" }, },
{ label: "Обратный Гарем", value: "210" }, } satisfies Filters;
{ label: "Офисные Работники", value: "200" }, }
{ label: "Пираты", value: "341" },
{ label: "Подземелья", value: "314" }, export default new RLIB();
{ label: "Политика", value: "311" },
{ label: "Полиция", value: "201" }, interface responseBook {
{ label: "Преступники / Криминал", value: "205" }, hasStickyPermission: boolean;
{ label: "Призраки / Духи", value: "196" }, bookmark?: null;
{ label: "Призыватели", value: "329" }, auth: boolean;
{ label: "Прыжки между мирами", value: "321" }, comments_version: string;
{ label: "Путешествие в другой мир", value: "318" }, manga: Manga;
{ label: "Путешествие во времени", value: "213" }, chapters: Chapters;
{ label: "Рабы", value: "355" }, user?: User;
{ label: "Ранги силы", value: "312" }, }
{ label: "Реинкарнация", value: "154" }, interface Manga {
{ label: "Самураи", value: "202" }, id: number;
{ label: "Скрытие личности", value: "315" }, name: string;
{ label: "Средневековье", value: "174" }, rusName?: string;
{ label: "Традиционные игры", value: "203" }, rus_name?: string;
{ label: "Умный ГГ", value: "303" }, engName?: string;
{ label: "Характерный рост", value: "332" }, slug: string;
{ label: "Хикикомори", value: "167" }, status: number;
{ label: "Эволюция", value: "322" }, chapters_count: number;
{ label: "Элементы РПГ", value: "327" }, altNames?: string[] | null;
{ label: "Эльфы", value: "217" }, coverImage?: string;
{ label: "Якудза", value: "165" }, href?: string;
], }
type: FilterTypes.ExcludableCheckboxGroup, interface Chapters {
}, list?: ListEntity[] | null;
} satisfies Filters; teams?: TeamsEntity[] | null;
} branches?: BranchesEntity[] | null;
is_paid?: null[] | null;
export default new RLIB(); }
interface ListEntity {
interface responseBook { chapter_id: number;
hasStickyPermission: boolean; chapter_slug: string;
bookmark?: null; chapter_name: string;
auth: boolean; chapter_number: string;
comments_version: string; chapter_volume: number;
manga: Manga; chapter_moderated: number;
chapters: Chapters; chapter_user_id: number;
user?: User; chapter_expired_at: string;
} chapter_scanlator_id: number;
interface Manga { chapter_created_at: string;
id: number; status?: null;
name: string; price: number;
rusName?: string; branch_id?: number;
rus_name?: string; username: string;
engName?: string; }
slug: string; interface TeamsEntity {
status: number; name: string;
chapters_count: number; alt_name: string;
altNames?: string[] | null; cover: string;
coverImage?: string; slug: string;
href?: string; id: number;
} branch_id: number;
interface Chapters { sale: number;
list?: ListEntity[] | null; href: string;
teams?: TeamsEntity[] | null; pivot: Pivot;
branches?: BranchesEntity[] | null; }
is_paid?: null[] | null; interface Pivot {
} manga_id: number;
interface ListEntity { team_id: number;
chapter_id: number; }
chapter_slug: string; interface BranchesEntity {
chapter_name: string; id: number;
chapter_number: string; manga_id: number;
chapter_volume: number; name: string;
chapter_moderated: number; teams?: TeamsEntity1[] | null;
chapter_user_id: number; is_subscribed: boolean;
chapter_expired_at: string; }
chapter_scanlator_id: number; interface TeamsEntity1 {
chapter_created_at: string; id: number;
status?: null; name: string;
price: number; slug: string;
branch_id: number; cover: string;
username: string; branch_id: number;
} is_active: number;
interface TeamsEntity { }
name: string; interface User {
alt_name: string; id: string;
cover: string; avatar: string;
slug: string; access: boolean;
id: number; isAdmin: boolean;
branch_id: number; paid: boolean;
sale: number; }
href: string;
pivot: Pivot;
}
interface Pivot {
manga_id: number;
team_id: number;
}
interface BranchesEntity {
id: number;
manga_id: number;
name: string;
teams?: TeamsEntity1[] | null;
is_subscribed: boolean;
}
interface TeamsEntity1 {
id: number;
name: string;
slug: string;
cover: string;
branch_id: number;
is_active: number;
}
interface User {
id: number;
avatar: string;
access: boolean;
isAdmin: boolean;
paid: boolean;
+406 -408
View File
@@ -1,408 +1,406 @@
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover"; import { defaultCover } from "@libs/defaultCover";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
import dayjs from "dayjs"; import dayjs from "dayjs";
class RNRF implements Plugin.PluginBase { class RNRF implements Plugin.PluginBase {
id = "RNRF"; id = "RNRF";
name = "РанобэРФ"; name = "РанобэРФ";
site = "https://ранобэ.рф"; site = "https://ранобэ.рф";
version = "1.0.0"; version = "1.0.0";
icon = "src/ru/ranoberf/icon.png"; icon = "src/ru/ranoberf/icon.png";
async popularNovels(
pageNo: number,
async popularNovels( { showLatestNovels, filters }: Plugin.PopularNovelsOptions,
pageNo: number, ): Promise<Plugin.NovelItem[]> {
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions, let url = this.site + "/books?order=";
): Promise<Plugin.NovelItem[]> { url += showLatestNovels
let url = this.site + "/books?order="; ? "lastPublishedChapter"
url += showLatestNovels : filters?.sort?.value || "popular";
? "lastPublishedChapter" url += "&page=" + pageNo;
: filters?.sort?.value || "popular";
url += "&page=" + pageNo; const result = await fetchApi(url);
const body = await result.text();
const result = await fetchApi(url);
const body = await result.text(); const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const loadedCheerio = parseHTML(body); const json: response = JSON.parse(jsonRaw || "{}");
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const json: response = JSON.parse(jsonRaw || "{}"); const novels: Plugin.NovelItem[] = [];
json.props.pageProps?.totalData?.items?.forEach((novel) =>
const novels: Plugin.NovelItem[] = []; novels.push({
json.props.pageProps?.totalData?.items?.forEach((novel) => name: novel.title,
novels.push({ cover: novel?.verticalImage?.url
name: novel.title, ? this.site + novel.verticalImage.url
cover: novel?.verticalImage?.url : defaultCover,
? this.site + novel.verticalImage.url url: this.site + "/" + novel.slug,
: defaultCover, }),
url: this.site + "/" + novel.slug, );
}),
); return novels;
}
return novels;
} async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(novelUrl);
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> { const body = await result.text();
const result = await fetchApi(novelUrl);
const body = await result.text(); const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const loadedCheerio = parseHTML(body); const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book;
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book; const novel: Plugin.SourceNovel = {
url: novelUrl,
const novel: Plugin.SourceNovel = { name: book?.title,
url: novelUrl, cover: book?.verticalImage?.url
name: book?.title, ? this.site + book.verticalImage.url
cover: book?.verticalImage?.url : defaultCover,
? this.site + book.verticalImage.url summary: book?.description,
: defaultCover, author: book?.author || "",
summary: book?.description, genres: book?.genres.map((item) => item.title).join(", "),
author: book?.author || "", status: book?.additionalInfo.includes("Активен")
genres: book?.genres.map((item) => item.title).join(", "), ? NovelStatus.Ongoing
status: book?.additionalInfo.includes("Активен") : NovelStatus.Completed,
? NovelStatus.Ongoing };
: NovelStatus.Completed,
}; const chapters: Plugin.ChapterItem[] = [];
const chapters: Plugin.ChapterItem[] = []; book?.chapters.forEach((chapter, chapterIndex) => {
if (!chapter.isDonate || chapter.isUserPaid) {
book?.chapters?.forEach((chapter) => { chapters.push({
if (!chapter.isDonate || chapter.isUserPaid) { name: chapter.title,
chapters.push({ url: this.site + chapter.url,
name: chapter.title, releaseTime: dayjs(chapter.publishedAt).format("LLL"),
releaseTime: dayjs(chapter.publishedAt).format("LLL"), chapterNumber: book.chapters.length - chapterIndex,
url: this.site + chapter.url, });
}); }
} });
});
novel.chapters = chapters.reverse();
novel.chapters = chapters.reverse(); return novel;
return novel; }
}
async parseChapter(chapterUrl: string): Promise<string> {
async parseChapter(chapterUrl: string): Promise<string> { const result = await fetchApi(chapterUrl).then((res) => res.text());
const result = await fetchApi(chapterUrl);
const body = await result.text(); let loadedCheerio = parseHTML(result);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
let loadedCheerio = parseHTML(body); const json: response = JSON.parse(jsonRaw || "{}");
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const json: response = JSON.parse(jsonRaw || "{}"); loadedCheerio = parseHTML(
json.props.pageProps?.chapter?.content?.text || "",
loadedCheerio = parseHTML( );
json.props.pageProps?.chapter?.content?.text || "",
); loadedCheerio("img").each((index, element) => {
if (!loadedCheerio(element).attr("src")?.startsWith("http")) {
const baseUrl = this.site; const src = loadedCheerio(element).attr("src");
loadedCheerio("img").each(function () { loadedCheerio(element).attr("src", this.site + src);
if (!loadedCheerio(this).attr("src")?.startsWith("http")) { }
const src = loadedCheerio(this).attr("src"); });
loadedCheerio(this).attr("src", baseUrl + src);
} const chapterText = loadedCheerio.html();
}); return chapterText;
}
const chapterText = loadedCheerio.html();
return chapterText; async searchNovels(
} searchTerm: string,
//pageNo: number | undefined = 1,
async searchNovels( ): Promise<Plugin.NovelItem[]> {
searchTerm: string, 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`;
//pageNo: number | undefined = 1, const result = await fetchApi(url);
): Promise<Plugin.NovelItem[]> { const body = (await result.json()) as { items: Item[] };
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 novels: Plugin.NovelItem[] = [];
const result = await fetchApi(url);
const body = (await result.json()) as { items: Item[] }; body.items.forEach((novel) =>
const novels: Plugin.NovelItem[] = []; novels.push({
name: novel.title,
body.items.forEach((novel) => cover: novel?.verticalImage?.url
novels.push({ ? this.site + novel.verticalImage.url
name: novel.title, : defaultCover,
cover: novel?.verticalImage?.url url: this.site + "/" + novel.slug,
? this.site + novel.verticalImage.url }),
: defaultCover, );
url: this.site + "/" + novel.slug,
}), return novels;
); }
fetchImage = fetchFile;
return novels;
} filters = {
fetchImage = fetchFile; sort: {
label: "Сортировка",
filters = { value: "popular",
sort: { options: [
label: "Сортировка", { label: "Рейтинг", value: "popular" },
value: "", { label: "Дате добавления", value: "new" },
options: [ { label: "Дате обновления", value: "lastPublishedChapter" },
{ label: "Рейтинг", value: "popular" }, { label: "Законченные", value: "completed" },
{ label: "Дате добавления", value: "new" }, ],
{ label: "Дате обновления", value: "lastPublishedChapter" }, type: FilterTypes.Picker,
{ label: "Законченные", value: "completed" }, },
], } satisfies Filters;
type: FilterTypes.Picker, }
}, export default new RNRF();
} satisfies Filters;
} interface response {
export default new RNRF(); props: Props;
page: string;
interface response { query: Query;
props: Props; buildId: string;
page: string; isFallback: boolean;
query: Query; gssp: boolean;
buildId: string; appGip: boolean;
isFallback: boolean; dynamicIds?: string[];
gssp: boolean; }
appGip: boolean;
dynamicIds?: string[]; interface Props {
} pageProps: PageProps;
isMobile: boolean;
interface Props { theme: string;
pageProps: PageProps; __N_SSP: boolean;
isMobile: boolean; }
theme: string;
__N_SSP: boolean; interface PageProps {
} initialReduxState: InitialReduxState;
book?: PagePropsBook;
interface PageProps { totalData?: TotalData;
initialReduxState: InitialReduxState; layoutViewCookies?: string;
book?: PagePropsBook; displayBanner?: boolean;
totalData?: TotalData; chapter?: PagePropsChapter;
layoutViewCookies?: string; }
displayBanner?: boolean;
chapter?: PagePropsChapter; interface PagePropsBook {
} id: number;
countryId: number;
interface PagePropsBook { creatorId: number;
id: number; lastChapterId: number;
countryId: number; sourceId: number;
creatorId: number; imgVerticalId: number;
lastChapterId: number; imgHorizontalId: number;
sourceId: number; imgSquareId: number;
imgVerticalId: number; imageVerticalId: number;
imgHorizontalId: number; imageHorizontalId: number;
imgSquareId: number; title: string;
imageVerticalId: number; titleEn: string;
imageHorizontalId: number; fullTitle: string;
title: string; fullTitleEn: string;
titleEn: string; description: string;
fullTitle: string; slug: string;
fullTitleEn: string; author: string;
description: string; additionalInfo: string;
slug: string; importantInfo: string;
author: string; status: string;
additionalInfo: string; views: number;
importantInfo: string; likes: number;
status: string; dislikes: number;
views: number; intervalSec: number;
likes: number; intervalForNextFreeChapterSec: number;
dislikes: number; chapterCost: number;
intervalSec: number; chapterPrice: number;
intervalForNextFreeChapterSec: number; chapterPriceCollected: number;
chapterCost: number; maxSubscriptionChapters: number;
chapterPrice: number; maxDonateChapters: number;
chapterPriceCollected: number; isDisplayOnMainPage: boolean;
maxSubscriptionChapters: number; withoutSchedule: boolean;
maxDonateChapters: number; nextFreeChapterPublishedAt: null;
isDisplayOnMainPage: boolean; createdAt: Date;
withoutSchedule: boolean; updatedAt: Date;
nextFreeChapterPublishedAt: null; userRating: null;
createdAt: Date; userBookmark: null;
updatedAt: Date; country: Country;
userRating: null; genres: Genre[];
userBookmark: null; timer: Timer;
country: Country; chapters: ChapterElement[];
genres: Genre[]; verticalImage: Image;
timer: Timer; horizontalImage: Image;
chapters: ChapterElement[]; squareImage: Image;
verticalImage: Image; }
horizontalImage: Image;
squareImage: Image; interface ChapterElement {
} id: number;
bookId: number;
interface ChapterElement { title: string;
id: number; url: string;
bookId: number; numberChapter: string;
title: string; tom: number | null;
url: string; chapterShortNumber: number | null;
numberChapter: string; isDonate: boolean;
tom: number | null; isSponsored: boolean;
chapterShortNumber: number | null; isSubscription: boolean;
isDonate: boolean; isEdited: boolean;
isSponsored: boolean; publishedAt: Date;
isSubscription: boolean; price: number;
isEdited: boolean; views: number;
publishedAt: Date; isUserPaid?: boolean;
price: number; }
views: number;
isUserPaid?: boolean; interface Country {
} id: number;
title: string;
interface Country { code: string;
id: number; }
title: string;
code: string; interface Genre {
} id: number;
title: string;
interface Genre { slug: string;
id: number; }
title: string;
slug: string; interface Image {
} id: number;
alt: string;
interface Image { path: string;
id: number; name: string;
alt: string; url: string;
path: string; }
name: string;
url: string; interface Timer {
} value: null;
for: null;
interface Timer { message: string;
value: null; }
for: null;
message: string; interface PagePropsChapter {
} id: number;
bookId: number;
interface PagePropsChapter { editorId: null;
id: number; translatorId: number;
bookId: number; parserChapterId: null;
editorId: null; title: string;
translatorId: number; slug: string;
parserChapterId: null; status: string;
title: string; tom: null;
slug: string; views: number;
status: string; isSponsored: boolean;
tom: null; isSubscription: boolean;
views: number; isDonate: boolean;
isSponsored: boolean; isIndexing: boolean;
isSubscription: boolean; publishedAt: Date;
isDonate: boolean; changeAvailabilityAt: null;
isIndexing: boolean; createdAt: Date;
publishedAt: Date; updatedAt: Date;
changeAvailabilityAt: null; numberChapter: string;
createdAt: Date; price: number;
updatedAt: Date; content: Content;
numberChapter: string; book: ChapterBook;
price: number; userBookmark: null;
content: Content; isUserPaid: boolean;
book: ChapterBook; nextChapter: null;
userBookmark: null; previousChapter: PreviousChapter;
isUserPaid: boolean; corrections: null;
nextChapter: null; }
previousChapter: PreviousChapter;
corrections: null; interface ChapterBook {
} id: number;
title: string;
interface ChapterBook { titleEn: string;
id: number; url: string;
title: string; }
titleEn: string;
url: string; interface Content {
} id: number;
text: string;
interface Content { symbolsCount: number;
id: number; }
text: string;
symbolsCount: number; interface PreviousChapter {
} id: number;
title: string;
interface PreviousChapter { views: number;
id: number; publishedAt: Date;
title: string; url: string;
views: number; numberChapter: string;
publishedAt: Date; }
url: string;
numberChapter: string; interface InitialReduxState {
} data?: Data;
auth: Auth;
interface InitialReduxState { banners?: Banners;
data?: Data; corrections?: Corrections;
auth: Auth; readerSettings?: ReaderSettings;
banners?: Banners; }
corrections?: Corrections;
readerSettings?: ReaderSettings; interface Auth {
} isAuth: boolean;
user: null;
interface Auth { }
isAuth: boolean;
user: null; interface Banners {
} displaySale: boolean;
}
interface Banners {
displaySale: boolean; interface Corrections {
} items: null;
data: null;
interface Corrections { }
items: null;
data: null; interface Data {
} bookPage: BookPage | null;
chapterPage: ChapterPage | null;
interface Data { }
bookPage: BookPage | null;
chapterPage: ChapterPage | null; interface BookPage {
} bookId: number;
bookmark: null;
interface BookPage { }
bookId: number;
bookmark: null; interface ChapterPage {
} chapterId: number;
bookId: number;
interface ChapterPage { bookmark: null;
chapterId: number; }
bookId: number;
bookmark: null; interface ReaderSettings {
} fontSize: number;
lineHeight: number;
interface ReaderSettings { textIndent: number;
fontSize: number; sidebarHide: number;
lineHeight: number; textAlign: string;
textIndent: number; }
sidebarHide: number;
textAlign: string; interface TotalData {
} items: Item[];
_links: Links;
interface TotalData { pagesData: PagesData;
items: Item[]; }
_links: Links;
pagesData: PagesData; interface Links {
} self: First;
first: First;
interface Links { last: First;
self: First; next: First;
first: First; }
last: First;
next: First; interface First {
} href: string;
}
interface First {
href: string; interface Item {
} id: number;
title: string;
interface Item { description: string;
id: number; slug: string;
title: string; url: string;
description: string; status: string;
slug: string; dislikes: number;
url: string; likes: number;
status: string; userRating: null;
dislikes: number; lastChapter: ChapterElement;
likes: number; verticalImage: Image;
userRating: null; horizontalImage: Image;
lastChapter: ChapterElement; squareImage: Image;
verticalImage: Image; }
horizontalImage: Image;
squareImage: Image; interface PagesData {
} totalCount: number;
pageCount: number;
interface PagesData { currentPage: number;
totalCount: number; perPage: number;
pageCount: number; }
currentPage: number;
perPage: number; interface Query {
} book?: string;
chapter?: string;
interface Query { }
book?: string;
chapter?: string;
+1081 -1105
View File
File diff suppressed because it is too large Load Diff
+286 -371
View File
@@ -1,371 +1,286 @@
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch"; import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs"; import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { defaultCover } from "@libs/defaultCover"; import { defaultCover } from "@libs/defaultCover";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
class TuNovelaLigera implements Plugin.PluginBase { class TuNovelaLigera implements Plugin.PluginBase {
id = "tunovelaligera"; id = "tunovelaligera";
name = "TuNovelaLigera"; name = "TuNovelaLigera";
icon = "src/es/tunovelaligera/icon.png"; icon = "src/es/tunovelaligera/icon.png";
site = "https://tunovelaligera.com/"; site = "https://tunovelaligera.com";
version = "1.0.0"; version = "1.0.0";
async popularNovels(
pageNo: number,
async popularNovels( {
pageNo: number, showLatestNovels,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters> filters,
): Promise<Plugin.NovelItem[]> { }: Plugin.PopularNovelsOptions<typeof this.filters>,
let link = `${this.site}`; ): Promise<Plugin.NovelItem[]> {
let link = this.site
link += filters.genres.value ? `genero/` + filters.genres.value : "novelas";
link += filters?.genres?.value ? "/genero" + filters.genres.value : "/novelas";
link += `/page/${pageNo}`; link += `/page/${pageNo}?m_orderby=`;
link += (showLatestNovels ? "latest" : filters?.order?.value || "rating");
link += filters.order.value;
const result = await fetchApi(link).then((res) => res.text());
const headers = new Headers(); const loadedCheerio = parseHTML(result);
const result = await fetchApi(link, { headers });
const body = await result.text(); const novels: Plugin.NovelItem[] = [];
const loadedCheerio = parseHTML(body); loadedCheerio(".page-item-detail").each((i, el) => {
const name = loadedCheerio(el).find(".h5 > a").text();
const novels: Plugin.NovelItem[] = []; const cover = loadedCheerio(el).find("img").attr("src");
const url = loadedCheerio(el).find(".h5 > a").attr("href");
loadedCheerio(".page-item-detail").each((i, el) => { if (!url) return;
const novelName = loadedCheerio(el).find(".h5 > a").text();
const novelCover = loadedCheerio(el).find("img").attr("src"); novels.push({ name, cover, url });
const novelUrl = loadedCheerio(el).find(".h5 > a").attr("href"); });
if (!novelUrl) return; return novels;
const novel = { }
name: novelName,
cover: novelCover, async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
url: novelUrl, const result = await fetchApi(novelUrl);
}; const body = await result.text();
novels.push(novel); let loadedCheerio = parseHTML(body);
});
const novel: Plugin.SourceNovel = {
return novels; url: novelUrl,
} chapters: [],
};
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const url = novelUrl; loadedCheerio(".manga-title-badges").remove();
const headers = new Headers();
const result = await fetchApi(url, { headers }); novel.name = loadedCheerio(".post-title > h1").text().trim();
const body = await result.text();
let novelCover = loadedCheerio(".summary_image > a > img");
let loadedCheerio = parseHTML(body);
novel.cover =
const novel: Plugin.SourceNovel = { novelCover.attr("data-src") ||
url, novelCover.attr("src") ||
chapters: [], novelCover.attr("data-cfsrc") ||
}; defaultCover;
loadedCheerio(".manga-title-badges").remove(); loadedCheerio(".post-content_item").each(function () {
const detailName = loadedCheerio(this)
novel.name = loadedCheerio(".post-title > h1").text().trim(); .find(".summary-heading > h5")
.text()
let novelCover = loadedCheerio(".summary_image > a > img"); .trim();
const detail = loadedCheerio(this).find(".summary-content").text().trim();
novel.cover =
novelCover.attr("data-src") || switch (detailName) {
novelCover.attr("src") || case "Género(s)":
novelCover.attr("data-cfsrc") || novel.genres = detail.replace(/, /g, ",");
defaultCover; break;
case "Autor(es)":
loadedCheerio(".post-content_item").each(function () { novel.author = detail;
const detailName = loadedCheerio(this) break;
.find(".summary-heading > h5") case "Estado":
.text() novel.status =
.trim(); detail.includes("OnGoing") || detail.includes("Updating")
const detail = loadedCheerio(this) ? NovelStatus.Ongoing
.find(".summary-content") : NovelStatus.Completed;
.text() break;
.trim(); }
});
switch (detailName) {
case "Generos": novel.summary = loadedCheerio("div.summary__content > p").text().trim();
novel.genres = detail.replace(/, /g, ",");
break; let chapter: Plugin.ChapterItem[] = [];
case "Autores":
novel.author = detail; const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
break; let lastPage = 1;
case "Estado": lastPage = +loadedCheerio(".lcp_paginator li:last").prev().text();
novel.status =
detail.includes("OnGoing") || const getChapters = async () => {
detail.includes("Updating") const n = novelUrl.split("/");
? NovelStatus.Ongoing const novelName = n[4];
: NovelStatus.Completed; const formData = new FormData();
break; formData.append("action", "madara_load_more");
} formData.append("page", "0");
}); formData.append("template", "html/loop/content");
formData.append("vars[category_name]", novelName);
novel.summary = loadedCheerio("div.summary__content > p").text().trim(); formData.append("vars[posts_per_page]", "10000");
let chapter: Plugin.ChapterItem[] = []; const result = await fetchApi(this.site + '/wp-admin/admin-ajax.php', {
method: "POST",
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); body: formData,
let lastPage = 1; });
lastPage = +loadedCheerio(".lcp_paginator li:last").prev().text(); const text = await result.text();
const getChapters = async () => { loadedCheerio = parseHTML(text);
const n = url.split("/");
const novelName = n[4]; loadedCheerio(".heading").each((i, el) => {
const formData = new FormData(); const chapterName = loadedCheerio(el)
formData.append("action", "madara_load_more"); .text()
formData.append("page", "0"); .replace(/[\t\n]/g, "")
formData.append("template", "html/loop/content"); .trim();
formData.append("vars[category_name]", novelName); const releaseDate = null;
formData.append("vars[posts_per_page]", "10000"); const chapterUrl = loadedCheerio(el).find("a").attr("href") || "";
const result = await fetchApi( chapter.push({
`${this.site}wp-admin/admin-ajax.php`, name: chapterName,
{ url: chapterUrl,
method: "POST", releaseTime: releaseDate,
body: formData, });
headers, });
} return chapter.reverse();
); };
const text = await result.text();
const getPageChapters = async () => {
loadedCheerio = parseHTML(text); for (let i = 1; i <= lastPage; i++) {
const chaptersUrl = `${novelUrl}?lcp_page0=${i}`;
loadedCheerio(".heading").each((i, el) => { const result = await fetchApi(chaptersUrl);
const chapterName = loadedCheerio(el) const chaptersHTML = await result.text();
.text()
.replace(/[\t\n]/g, "") let chapterCheerio = parseHTML(chaptersHTML);
.trim();
const releaseDate = null; chapterCheerio("#lcp_instance_0 li").each((i, el) => {
const chapterUrl = const chapterName = chapterCheerio(el)
loadedCheerio(el).find("a").attr("href") || ""; .find("a")
.text()
chapter.push({ .replace(/[\t\n]/g, "")
name: chapterName, .trim();
url: chapterUrl,
releaseTime: releaseDate, const releaseDate = chapterCheerio(el).find("span").text().trim();
});
}); const chapterUrl = chapterCheerio(el).find("a").attr("href") || "";
return chapter.reverse();
}; chapter.push({
name: chapterName,
const getPageChapters = async () => { releaseTime: releaseDate,
for (let i = 1; i <= lastPage; i++) { url: chapterUrl,
const chaptersUrl = `${novelUrl}?lcp_page0=${i}`; });
const result = await fetchApi(chaptersUrl, { headers }); });
const chaptersHTML = await result.text(); await delay(1000);
}
let chapterCheerio = parseHTML(chaptersHTML); return chapter.reverse();
};
chapterCheerio(".lcp_catlist li").each((i, el) => {
const chapterName = chapterCheerio(el) novel.chapters = await getChapters();
.find("a")
.text() if (!novel.chapters.length) {
.replace(/[\t\n]/g, "") await delay(1000);
.trim(); novel.chapters = await getPageChapters();
}
const releaseDate = chapterCheerio(el)
.find("span") return novel;
.text() }
.trim();
async parseChapter(chapterUrl: string): Promise<string> {
const chapterUrl = const result = await fetchApi(chapterUrl);
chapterCheerio(el).find("a").attr("href") || ""; const body = await result.text();
chapter.push({ const loadedCheerio = parseHTML(body);
name: chapterName,
releaseTime: releaseDate, loadedCheerio("#hola_siguiente").next().find("div").remove();
url: chapterUrl, const chapterText = loadedCheerio("#hola_siguiente").next().html() || "";
});
}); return chapterText;
await delay(1000); }
}
return chapter.reverse(); async searchNovels(
}; searchTerm: string,
): Promise<Plugin.NovelItem[]> {
novel.chapters = await getChapters(); const url = `${this.site}/?s=${searchTerm}&post_type=wp-manga`;
if (!novel.chapters.length) { const result = await fetchApi(url);
await delay(1000); const body = await result.text();
novel.chapters = await getPageChapters();
} const loadedCheerio = parseHTML(body);
return novel; const novels: Plugin.NovelItem[] = [];
}
loadedCheerio(".c-tabs-item__content").each((i, el) => {
async parseChapter(chapterUrl: string): Promise<string> { const name = loadedCheerio(el).find(".h4 > a").text();
const headers = new Headers(); const cover = loadedCheerio(el).find("img").attr("src");
const result = await fetchApi(chapterUrl); const url = loadedCheerio(el).find(".h4 > a").attr("href");
const body = await result.text(); if (!url) return;
const loadedCheerio = parseHTML(body); novels.push({ name, cover, url });
});
loadedCheerio("#hola_siguiente").next().find("div").remove();
const chapterText = return novels;
loadedCheerio("#hola_siguiente").next().html() || ""; }
return chapterText; async fetchImage(url: string): Promise<string | undefined> {
} return await fetchFile(url);
}
async searchNovels(
searchTerm: string, filters = {
pageNo?: number | undefined order: {
): Promise<Plugin.NovelItem[]> { value: "rating",
const url = `${this.site}?s=${searchTerm}&post_type=wp-manga`; label: "Ordenado por",
options: [
const result = await fetchApi(url); { label: "Lo mas reciente", value: "latest" },
const body = await result.text(); { label: "A-Z", value: "alphabet" },
{ label: "Clasificación", value: "rating" },
const loadedCheerio = parseHTML(body); { label: "Trending", value: "trending" },
{ label: "Mas visto", value: "views" },
const novels: Plugin.NovelItem[] = []; { label: "Nuevo", value: "new-manga" },
],
loadedCheerio(".c-tabs-item__content").each((i, el) => { type: FilterTypes.Picker,
const novelName = loadedCheerio(el).find(".h4 > a").text(); },
const novelCover = loadedCheerio(el).find("img").attr("src"); genres: {
const novelUrl = loadedCheerio(el).find(".h4 > a").attr("href"); value: "",
if (!novelUrl) return; label: "Generos",
novels.push({ options: [
name: novelName, { label: "None", value: "" },
url: novelUrl, { label: "Acción", value: "accion" },
cover: novelCover, { label: "Adulto", value: "adulto" },
}); { label: "Artes Marciales", value: "artes-marciales" },
}); { label: "Aventura", value: "aventura" },
{ label: "Ciencia Ficción", value: "ciencia-ficcion" },
return novels; { label: "Comedia", value: "comedia" },
} { label: "Deportes", value: "deportes" },
{ label: "Drama", value: "drama" },
async fetchImage(url: string): Promise<string | undefined> { { label: "Eastern Fantasy", value: "eastern-fantasy" },
return await fetchFile(url); { label: "Ecchi", value: "ecchi" },
} { label: "FanFiction", value: "fan-fiction" },
{ label: "Fantasía", value: "fantasia" },
filters = { { label: "Fantasía oriental", value: "fantasia-oriental" },
order: { { label: "Ficción Romántica", value: "ficcion-romantica" },
value: "?m_orderby=rating", { label: "Gender Bender", value: "gender-bender" },
label: "Ordenado por", { label: "Harem", value: "harem" },
options: [ { label: "Histórico", value: "historico" },
{ label: "Lo mas reciente", value: "?m_orderby=latest" }, { label: "Horror", value: "horror" },
{ label: "Josei", value: "josei" },
{ label: "A-Z", value: "?m_orderby=alphabet" }, { label: "Maduro", value: "maduro" },
{ label: "Mecha", value: "mecha" },
{ label: "Clasificación", value: "?m_orderby=rating" }, { label: "Misterio", value: "misterio" },
{ label: "Novela China", value: "novela-china" },
{ label: "Trending", value: "?m_orderby=trending" }, { label: "Novela FanFiction", value: "novela-fanfiction" },
{ label: "Novela Japonesa", value: "novela-japonesa" },
{ label: "Mas visto", value: "?m_orderby=views" }, { label: "Novela Koreana", value: "novela-koreana" },
{ label: "Novela ligera", value: "novela-ligera" },
{ label: "Nuevo", value: "?m_orderby=new-manga" }, { label: "Novela original", value: "novela-original" },
], { label: "Novela Web", value: "web-novel" },
type: FilterTypes.Picker, { label: "Psicológico", value: "psicologico" },
}, { label: "Realismo Mágico", value: "realismo-magico" },
genres: { { label: "Recuento de vida", value: "recuento-de-vida" },
value: "", { label: "Romance", value: "romance" },
label: "Generos", { label: "Romance contemporáneo", value: "romance-contemporaneo" },
options: [ { label: "Romance Moderno", value: "romance-moderno" },
{ label: "None", value: "" }, { label: "Seinen", value: "seinen" },
{ label: "Acción", value: "accion" }, { label: "Shoujo", value: "shoujo" },
{ label: "Shounen", value: "shounen" },
{ label: "Adulto", value: "adulto" }, { label: "Sobrenatural", value: "sobrenatural" },
{ label: "Tragedia", value: "tragedia" },
{ label: "Artes Marciales", value: "artes-marciales" }, { label: "Vampiros", value: "vampiros" },
{ label: "Vida Escolar", value: "vida-escolar" },
{ label: "Aventura", value: "aventura" }, { label: "Western Fantasy", value: "western-fantasy" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Ciencia Ficción", value: "ciencia-ficcion" }, { label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Comedia", value: "comedia" }, { label: "Yaoi", value: "yaoi" },
],
{ label: "Deportes", value: "deportes" }, type: FilterTypes.Picker,
},
{ label: "Drama", value: "drama" }, } satisfies Filters;
}
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
export default new TuNovelaLigera();
{ label: "Ecchi", value: "ecchi" },
{ label: "FanFiction", value: "fan-fiction" },
{ label: "Fantasía", value: "fantasia" },
{ label: "Fantasía oriental", value: "fantasia-oriental" },
{ label: "Ficción Romántica", value: "ficcion-romantica" },
{ label: "Gender Bender", value: "gender-bender" },
{ label: "Harem", value: "harem" },
{ label: "Histórico", value: "historico" },
{ label: "Horror", value: "horror" },
{ label: "Josei", value: "josei" },
{ label: "Maduro", value: "maduro" },
{ label: "Mecha", value: "mecha" },
{ label: "Misterio", value: "misterio" },
{ label: "Novela China", value: "novela-china" },
{ label: "Novela FanFiction", value: "novela-fanfiction" },
{ label: "Novela Japonesa", value: "novela-japonesa" },
{ label: "Novela Koreana", value: "novela-koreana" },
{ label: "Novela ligera", value: "novela-ligera" },
{ label: "Novela original", value: "novela-original" },
{ label: "Novela Web", value: "web-novel" },
{ label: "Psicológico", value: "psicologico" },
{ label: "Realismo Mágico", value: "realismo-magico" },
{ label: "Recuento de vida", value: "recuento-de-vida" },
{ label: "Romance", value: "romance" },
{
label: "Romance contemporáneo",
value: "romance-contemporaneo",
},
{ label: "Romance Moderno", value: "romance-moderno" },
{ label: "Seinen", value: "seinen" },
{ label: "Shoujo", value: "shoujo" },
{ label: "Shounen", value: "shounen" },
{ label: "Sobrenatural", value: "sobrenatural" },
{ label: "Tragedia", value: "tragedia" },
{ label: "Vampiros", value: "vampiros" },
{ label: "Vida Escolar", value: "vida-escolar" },
{ label: "Western Fantasy", value: "western-fantasy" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Yaoi", value: "yaoi" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new TuNovelaLigera();
+22 -22
View File
@@ -22,7 +22,7 @@ class IfreedomPlugin implements Plugin.PluginBase {
constructor(metadata: IfreedomMetadata) { constructor(metadata: IfreedomMetadata) {
this.id = metadata.id; this.id = metadata.id;
this.name = metadata.sourceName + "[ifreedom]"; this.name = metadata.sourceName;
this.icon = `multisrc/ifreedom/icons/${metadata.id}.png`; this.icon = `multisrc/ifreedom/icons/${metadata.id}.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = "1.0.0"; this.version = "1.0.0";
@@ -31,20 +31,17 @@ class IfreedomPlugin implements Plugin.PluginBase {
async popularNovels( async popularNovels(
page: number, page: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions, { filters, showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
let url = this.site + "/vse-knigi/?sort=" + let url = this.site + "/vse-knigi/?sort=" +
(showLatestNovels ? "По дате обновления" : filters?.sort?.value || "По рейтингу"); (showLatestNovels ? "По дате обновления" : filters?.sort?.value || "По рейтингу");
if (filters?.status?.value instanceof Array) { Object.entries(filters || {}).forEach(([type, { value }]) => {
url += filters.status.value.map((i) => "&status[]=" + i).join(""); if (value instanceof Array && value.length) {
} url += "&" + type + "[]=" + value.join("&" + type + "[]=");
if (filters?.lang?.value instanceof Array) { }
url += filters.lang.value.map((i) => "&lang[]=" + i).join(""); });
}
if (filters?.genre?.value instanceof Array) {
url += filters.genre.value.map((i) => "&genre[]=" + i).join("");
}
url += "&bpage=" + page; url += "&bpage=" + page;
const body = await fetchApi(url).then((res) => res.text()); const body = await fetchApi(url).then((res) => res.text());
@@ -102,14 +99,14 @@ class IfreedomPlugin implements Plugin.PluginBase {
if (novel.author == "Не указан") delete novel.author; if (novel.author == "Не указан") delete novel.author;
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
loadedCheerio("div.li-ranobe").each(function () { const totalChapters = loadedCheerio("div.li-ranobe").length;
const name = loadedCheerio(this).find("a").text();
const url = loadedCheerio(this).find("a").attr("href");
if (!loadedCheerio(this).find("label.buy-ranobe").length && name && url) {
const releaseTime = loadedCheerio(this)
.find("div.li-col2-ranobe").text().trim();
chapters.push({ name, releaseTime, url }); loadedCheerio("div.li-ranobe").each((chapterIndex, element) => {
const name = loadedCheerio(element).find("a").text();
const url = loadedCheerio(element).find("a").attr("href");
if (!loadedCheerio(element).find("label.buy-ranobe").length && name && url) {
const releaseTime = loadedCheerio(element).find("div.li-col2-ranobe").text().trim();
chapters.push({ name, url, releaseTime, chapterNumber: totalChapters - chapterIndex });
} }
}); });
@@ -121,11 +118,14 @@ class IfreedomPlugin implements Plugin.PluginBase {
const body = await fetchApi(chapterUrl).then((res) => res.text()); const body = await fetchApi(chapterUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
loadedCheerio(".entry-content img").each(function () { loadedCheerio(".entry-content img").each((index, element) => {
const srcset = loadedCheerio(this).attr("srcset")?.split?.(" "); const srcset = loadedCheerio(element).attr("srcset")?.split?.(" ");
if (srcset?.length) { if (srcset?.length) {
const bestlink = srcset.filter((url) => url.startsWith("http")).unshift(); loadedCheerio(element).removeAttr("srcset");
if (typeof bestlink == 'string') loadedCheerio(this).attr("src", bestlink); const bestlink: string[] = srcset.filter((url) => url.startsWith("http"));
if (bestlink[bestlink.length - 1]) {
loadedCheerio(element).attr("src", bestlink[bestlink.length - 1]);
}
} }
}); });
+3
View File
@@ -11,6 +11,9 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
/* This map is changing `type` from string to FilterTypes */ /* This map is changing `type` from string to FilterTypes */
.map<MadaraMetadata>((p) => { .map<MadaraMetadata>((p) => {
let d = false; let d = false;
if (p?.filters?.genres?.options?.length) {
p.filters.genres.options.unshift({ label: "NONE", value: "" })
}
const filters: Filters = {}; const filters: Filters = {};
for (const k in p.filters) { for (const k in p.filters) {
const f = p.filters[k as keyof typeof p.filters]; const f = p.filters[k as keyof typeof p.filters];
File diff suppressed because it is too large Load Diff
+285 -239
View File
@@ -4,275 +4,321 @@ import { Plugin } from "@typings/plugin";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
import { defaultCover } from "@libs/defaultCover"; import { defaultCover } from "@libs/defaultCover";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { parseMadaraDate } from "@libs/parseMadaraDate";
import dayjs from "dayjs"; import dayjs from "dayjs";
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
const includesAny = (str: string, keywords: string[]) =>
new RegExp(keywords.join("|")).test(str);
interface MadaraOptionPath { interface MadaraOptionPath {
genres?: string; genres?: string;
novels?: string; novels?: string;
novel?: string; novel?: string;
chapter?: string; chapter?: string;
} }
const MadaraDefaultPath = { const MadaraDefaultPath = {
genres: "novel-genre", genres: "novel-genre",
novels: "novel", novels: "novel",
novel: "novel", novel: "novel",
chapter: "novel", chapter: "novel",
}; };
interface MadaraOptions { interface MadaraOptions {
useNewChapterEndpoint?: boolean; useNewChapterEndpoint?: boolean;
path?: MadaraOptionPath; path?: MadaraOptionPath;
lang?: string; lang?: string;
orderBy?: string; orderBy?: string;
} }
export interface MadaraMetadata { export interface MadaraMetadata {
id: string; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
options?: MadaraOptions; options?: MadaraOptions;
filters?: Filters; filters?: Filters;
} }
class MadaraPlugin implements Plugin.PluginBase { class MadaraPlugin implements Plugin.PluginBase {
id: string; id: string;
name: string; name: string;
icon: string; icon: string;
site: string; site: string;
version: string; version: string;
options?: MadaraOptions; options?: MadaraOptions;
filters?: Filters | undefined; filters?: Filters | undefined;
constructor(metadata: MadaraMetadata) { constructor(metadata: MadaraMetadata) {
this.id = metadata.id; this.id = metadata.id;
this.name = metadata.sourceName + "[madara]"; this.name = metadata.sourceName;
const iconFileName = metadata.sourceName const iconFileName = metadata.sourceName.replace(/\s+/g, "").toLowerCase();
.replace(/\s+/g, "") this.icon = `multisrc/madara/icons/${iconFileName}.png`;
.toLowerCase(); this.site = metadata.sourceSite;
this.icon = `multisrc/madara/icons/${iconFileName}.png`; this.version = "1.0.0";
this.site = metadata.sourceSite; this.options = metadata.options;
this.version = "1.0.0"; this.filters = metadata.filters;
this.options = metadata.options; }
this.filters = metadata.filters; async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
let url = this.site;
if (filters?.genres?.value) {
url +=
(this.options?.path?.genres || MadaraDefaultPath.genres) +
"/" + filters.genres.value;
} else {
url += this.options?.path?.novels || MadaraDefaultPath.novels;
} }
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
let url = this.site; url += "/page/" + pageNo + "/?m_orderby=" +
if (filters?.genres?.value) { (showLatestNovels ? "latest" : filters?.sort?.value || "rating");
url += (this.options?.path?.genres || MadaraDefaultPath.genres) +
"/" +
filters.genres.value;
} else {
url += this.options?.path?.novels || MadaraDefaultPath.novels;
}
url += const body = await fetchApi(url).then((res) => res.text());
"/page/" + const loadedCheerio = parseHTML(body);
pageNo +
"/" +
"?m_orderby=" +
(showLatestNovels ? "latest" : filters?.sort?.value || "rating");
const body = await fetchApi(url).then((res) => res.text()); loadedCheerio(".manga-title-badges").remove();
const loadedCheerio = parseHTML(body); loadedCheerio(".page-item-detail").each(function () {
const novelName = loadedCheerio(this).find(".post-title").text().trim();
const image = loadedCheerio(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
const novelUrl =
loadedCheerio(this).find(".post-title").find("a").attr("href") || "";
if (!novelName || !novelUrl) return;
loadedCheerio(".manga-title-badges").remove(); const novel: Plugin.NovelItem = {
name: novelName,
cover: novelCover,
url: novelUrl,
};
loadedCheerio(".page-item-detail").each(function () { novels.push(novel);
const novelName = loadedCheerio(this) });
.find(".post-title")
.text()
.trim();
let image = loadedCheerio(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
let novelUrl = return novels;
loadedCheerio(this) }
.find(".post-title") async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
.find("a") const novel: Plugin.SourceNovel = {
.attr("href") || ""; url: novelUrl,
const novel: Plugin.NovelItem = { };
name: novelName,
cover: novelCover,
url: novelUrl,
};
novels.push(novel); const body = await fetchApi(novelUrl).then((res) => res.text());
});
return novels; let loadedCheerio = parseHTML(body);
loadedCheerio(".manga-title-badges, #manga-title span").remove();
novel.name =
loadedCheerio(".post-title h1").text().trim() ||
loadedCheerio("#manga-title h1").text();
novel.cover =
loadedCheerio(".summary_image > a > img").attr("data-lazy-src") ||
loadedCheerio(".summary_image > a > img").attr("data-src") ||
loadedCheerio(".summary_image > a > img").attr("src") ||
loadedCheerio(".summary_image > a > img").attr("src") ||
defaultCover;
loadedCheerio(".post-content_item, .post-content").each(function () {
const detailName = loadedCheerio(this).find("h5").text().trim();
const detail = loadedCheerio(this).find(".summary-content").text().trim();
switch (detailName) {
case "Genre(s)":
case "Género(s)":
case "التصنيفات":
novel.genres = detail;
break;
case "Author(s)":
case "Autor(es)":
case "المؤلف":
case "المؤلف (ين)":
novel.author = detail;
break;
case "Status":
case "Estado":
novel.status =
detail.includes("OnGoing") || detail.includes("مستمرة")
? NovelStatus.Ongoing
: NovelStatus.Completed;
break;
}
});
loadedCheerio("div.summary__content .code-block,script").remove();
novel.summary =
loadedCheerio("div.summary__content").text().trim() ||
loadedCheerio("#tab-manga-about").text().trim() ||
loadedCheerio('.post-content_item h5:contains("Summary")')
.next()
.text()
.trim();
const chapters: Plugin.ChapterItem[] = [];
let html = "";
if (this.options?.useNewChapterEndpoint) {
html = await fetchApi(novelUrl + "ajax/chapters/", {
method: "POST",
}).then((res) => res.text());
} else {
const novelId =
loadedCheerio(".rating-post-id").attr("value") ||
loadedCheerio("#manga-chapters-holder").attr("data-id") ||
"";
const formData = new FormData();
formData.append("action", "manga_get_chapters");
formData.append("manga", novelId);
html = await fetchApi(this.site + "wp-admin/admin-ajax.php", {
method: "POST",
body: formData,
}).then((res) => res.text());
} }
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = {
url: novelUrl,
};
const body = await fetchApi(novelUrl).then((res) => res.text()); if (html !== "0") {
loadedCheerio = parseHTML(html);
let loadedCheerio = parseHTML(body);
loadedCheerio(".manga-title-badges, #manga-title span").remove();
novel.name =
loadedCheerio(".post-title h1").text().trim() ||
loadedCheerio("#manga-title h1").text();
novel.cover =
loadedCheerio(".summary_image > a > img").attr("data-lazy-src") ||
loadedCheerio(".summary_image > a > img").attr("data-src") ||
loadedCheerio(".summary_image > a > img").attr("src") ||
defaultCover;
loadedCheerio(".post-content_item, .post-content").each(function () {
const detailName = loadedCheerio(this).find("h5").text().trim();
const detail = loadedCheerio(this)
.find(".summary-content")
.text()
.trim();
switch (detailName) {
case "Genre(s)":
case "التصنيفات":
novel.genres = detail.replace(/[\\t\\n]/g, ",");
break;
case "Author(s)":
case "المؤلف":
case "المؤلف (ين)":
novel.author = detail;
break;
case "Status":
case "الحالة":
novel.status =
detail.includes("OnGoing") || detail.includes("مستمرة")
? NovelStatus.Ongoing
: NovelStatus.Completed;
break;
}
});
loadedCheerio("div.summary__content .code-block,script").remove();
novel.summary =
loadedCheerio("div.summary__content").text().trim() ||
loadedCheerio("#tab-manga-about").text().trim() ||
loadedCheerio('.post-content_item h5:contains("Summary")')
.next()
.text()
.trim();
let html;
if (this.options?.useNewChapterEndpoint !== true) {
const novelId =
loadedCheerio(".rating-post-id").attr("value") ||
loadedCheerio("#manga-chapters-holder").attr("data-id") ||
"";
const formData = new FormData();
formData.append("action", "manga_get_chapters");
formData.append("manga", novelId);
html = await fetchApi(this.site + "wp-admin/admin-ajax.php", {
method: "POST",
body: formData,
}).then((res) => res.text());
} else {
html = await fetchApi(novelUrl + "ajax/chapters/", {
method: "POST",
}).then((res) => res.text());
}
if (html !== "0") {
loadedCheerio = parseHTML(html);
}
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio(".wp-manga-chapter").each(function () {
const chapterName = loadedCheerio(this).find("a").text().trim();
let releaseDate = null;
releaseDate = loadedCheerio(this)
.find("span.chapter-release-date")
.text()
.trim();
if (releaseDate) {
releaseDate = parseMadaraDate(releaseDate);
} else {
/**
* Insert current date
*/
releaseDate = dayjs().format("LL");
}
let chapterUrl = loadedCheerio(this).find("a").attr("href") || "";
chapters.push({
name: chapterName,
releaseTime: releaseDate,
url: chapterUrl,
});
});
novel.chapters = chapters.reverse();
return novel;
} }
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body); const totalChapters = loadedCheerio(".wp-manga-chapter").length;
const chapterText = loadedCheerio(".wp-manga-chapter").each((chapterIndex, element) => {
loadedCheerio(".text-left").html() || const chapterName = loadedCheerio(element).find("a").text().trim();
loadedCheerio(".text-right").html() ||
loadedCheerio(".entry-content").html() ||
"";
return chapterText; let releaseDate = loadedCheerio(element)
.find("span.chapter-release-date")
.text()
.trim();
if (releaseDate) {
releaseDate = this.parseData(releaseDate);
} else {
releaseDate = dayjs().format("LL");
}
let chapterUrl = loadedCheerio(element).find("a").attr("href") || "";
chapters.push({
name: chapterName,
url: chapterUrl,
releaseTime: releaseDate || null,
chapterNumber: totalChapters - chapterIndex,
});
});
novel.chapters = chapters.reverse();
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((res) => res.text());
const loadedCheerio = parseHTML(body);
const chapterText =
loadedCheerio(".text-left").html() ||
loadedCheerio(".text-right").html() ||
loadedCheerio(".entry-content").html() ||
loadedCheerio(".c-blog-post > div > div:nth-child(2)").html() ||
"";
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo?: number | undefined,
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const url = this.site + "?s=" + searchTerm + "&post_type=wp-manga";
const body = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(body);
loadedCheerio(".c-tabs-item__content").each(function () {
const novelName = loadedCheerio(this).find(".post-title").text().trim();
let image = loadedCheerio(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
let novelUrl =
loadedCheerio(this).find(".post-title").find("a").attr("href") || "";
const novel = {
name: novelName,
cover: novelCover,
url: novelUrl,
};
novels.push(novel);
});
return novels;
}
fetchImage = fetchFile;
parseData = (date: string) => {
const dayJSDate = dayjs(); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
const timeAgoInt = parseInt(timeAgo, 10);
if (!timeAgo) return date; // there is no number!
if (includesAny(date, ["detik", "segundo", "second", "วินาที"])) {
dayJSDate.subtract(timeAgoInt, "second"); // go back N seconds
} else if (
includesAny(date, [
"menit",
"dakika",
"min",
"minute",
"minuto",
"นาที",
"دقائق",
])
) {
dayJSDate.subtract(timeAgoInt, "minute"); // go back N minute
} else if (
includesAny(date, [
"jam",
"saat",
"heure",
"hora",
"hour",
"ชั่วโมง",
"giờ",
"ore",
"ساعة",
"小时",
])
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
} else if (
includesAny(date, [
"hari",
"gün",
"jour",
"día",
"dia",
"day",
"วัน",
"ngày",
"giorni",
"أيام",
"天",
])
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
} else if (includesAny(date, ["week", "semana"])) {
dayJSDate.subtract(timeAgoInt, "week"); // go back N a week
} else if (includesAny(date, ["month", "mes"])) {
dayJSDate.subtract(timeAgoInt, "month"); // go back N months
} else if (includesAny(date, ["year", "año"])) {
dayJSDate.subtract(timeAgoInt, "year"); // go back N years
} else {
return date;
} }
async searchNovels(
searchTerm: string,
pageNo?: number | undefined
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const url = this.site + "?s=" + searchTerm + "&post_type=wp-manga";
const body = await fetchApi(url).then((res) => res.text()); return dayJSDate.format("LL");
};
const loadedCheerio = parseHTML(body);
loadedCheerio(".c-tabs-item__content").each(function () {
const novelName = loadedCheerio(this)
.find(".post-title")
.text()
.trim();
let image = loadedCheerio(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
let novelUrl =
loadedCheerio(this)
.find(".post-title")
.find("a")
.attr("href") || "";
const novel = {
name: novelName,
cover: novelCover,
url: novelUrl,
};
novels.push(novel);
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url, {});
}
} }
-4
View File
@@ -76,10 +76,6 @@
"label": "Fantasy Romance", "label": "Fantasy Romance",
"value": "fantasy-romance" "value": "fantasy-romance"
}, },
{
"label": "Game",
"value": "game"
},
{ {
"label": "Gender Bender", "label": "Gender Bender",
"value": "gender-bender" "value": "gender-bender"
File diff suppressed because it is too large Load Diff
+50 -21
View File
@@ -3,8 +3,7 @@ import { Filters, FilterTypes } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin"; import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus"; import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio"; import { load as parseHTML } from "cheerio";
import { parseMadaraDate } from "@libs/parseMadaraDate"; import dayjs from "dayjs";
// import { defaultCover } from "@libs/defaultCover";
export interface ReadwnMetadata { export interface ReadwnMetadata {
id: string; id: string;
@@ -23,9 +22,8 @@ class ReadwnPlugin implements Plugin.PluginBase {
constructor(metadata: ReadwnMetadata) { constructor(metadata: ReadwnMetadata) {
this.id = metadata.id; this.id = metadata.id;
this.name = metadata.sourceName + "[readwn]"; this.name = metadata.sourceName;
const iconFileName = metadata.sourceName.replace(/\s+/g, "").toLowerCase(); this.icon = `multisrc/readwn/icons/${metadata.id}.png`;
this.icon = `multisrc/readwn/icons/${iconFileName}.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = "1.0.0"; this.version = "1.0.0";
this.filters = metadata.filters; this.filters = metadata.filters;
@@ -70,7 +68,8 @@ class ReadwnPlugin implements Plugin.PluginBase {
novel.name = loadedCheerio("h1.novel-title").text(); novel.name = loadedCheerio("h1.novel-title").text();
novel.author = loadedCheerio("span[itemprop=author]").text(); novel.author = loadedCheerio("span[itemprop=author]").text();
novel.cover = this.site + loadedCheerio("figure.cover > img").attr("data-src"); novel.cover =
this.site + loadedCheerio("figure.cover > img").attr("data-src");
novel.summary = loadedCheerio(".summary") novel.summary = loadedCheerio(".summary")
.text() .text()
@@ -84,7 +83,10 @@ class ReadwnPlugin implements Plugin.PluginBase {
loadedCheerio("div.header-stats > span").each(function () { loadedCheerio("div.header-stats > span").each(function () {
if (loadedCheerio(this).find("small").text() === "Status") { if (loadedCheerio(this).find("small").text() === "Status") {
novel.status = loadedCheerio(this).find("strong").text(); novel.status =
loadedCheerio(this).find("strong").text() === "Ongoing"
? NovelStatus.Ongoing
: NovelStatus.Completed;
} }
}); });
@@ -97,17 +99,48 @@ class ReadwnPlugin implements Plugin.PluginBase {
); );
const chapters: Plugin.ChapterItem[] = loadedCheerio(".chapter-list li") const chapters: Plugin.ChapterItem[] = loadedCheerio(".chapter-list li")
.map((index, element) => { .map((chapterIndex, element) => {
const name = loadedCheerio(element).find("a .chapter-title").text().trim(); const name = loadedCheerio(element).find("a .chapter-title").text().trim();
const url = loadedCheerio(element).find("a").attr("href")?.trim(); const url = loadedCheerio(element).find("a").attr("href")?.trim();
const releaseTime = loadedCheerio(element).find("a .chapter-update").text().trim();
if (!name || !url) return null; if (!name || !url) return null;
let releaseTime = loadedCheerio(element).find("a .chapter-update").text().trim();
if (releaseTime?.includes?.("ago")) {
const timeAgo = releaseTime.match(/\d+/)?.[0] || "0";
const timeAgoInt = parseInt(timeAgo, 10);
if (timeAgoInt) {
const dayJSDate = dayjs(); // today
if (
releaseTime.includes("hours ago") ||
releaseTime.includes("hour ago")
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
}
if (
releaseTime.includes("days ago") ||
releaseTime.includes("day ago")
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
}
if (
releaseTime.includes("months ago") ||
releaseTime.includes("month ago")
) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
}
releaseTime = dayJSDate.format("LL");
}
}
return { return {
name, name,
releaseTime: parseMadaraDate(releaseTime),
url: this.site + url, url: this.site + url,
releaseTime: releaseTime,
chapterNumber: chapterIndex + 1,
}; };
}) })
.get() .get()
@@ -122,8 +155,9 @@ class ReadwnPlugin implements Plugin.PluginBase {
for (let i = (lastChapterNo || chapters.length) + 1; i <= latestChapterNo; i++) { for (let i = (lastChapterNo || chapters.length) + 1; i <= latestChapterNo; i++) {
chapters.push({ chapters.push({
name: "Chapter " + i, name: "Chapter " + i,
releaseTime: null,
url: novelUrl.replace(".html", "_" + i + ".html"), url: novelUrl.replace(".html", "_" + i + ".html"),
releaseTime: null,
chapterNumber: i,
}); });
} }
} }
@@ -140,22 +174,17 @@ class ReadwnPlugin implements Plugin.PluginBase {
return chapterText; return chapterText;
} }
async searchNovels(keyboard: string): Promise<Plugin.NovelItem[]> { async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.site + "/e/search/index.php", { const result = await fetchApi(this.site + "/e/search/index.php", {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
Referer: this.site + "/search.html", Referer: this.site + "/search.html",
Origin: this.site, Origin: this.site,
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
}, },
method: "POST", method: "POST",
body: JSON.stringify({ body:
show: "title", "show=title&tempid=1&tbname=news&keyboard=" +
tempid: 1, encodeURIComponent(searchTerm),
tbname: "news",
keyboard,
}),
}).then((res) => res.text()); }).then((res) => res.text());
const loadedCheerio = parseHTML(result); const loadedCheerio = parseHTML(result);
+2 -2
View File
@@ -1552,7 +1552,7 @@
], ],
"type": "Picker" "type": "Picker"
}, },
"trash": { "extra": {
"label": "Другое:", "label": "Другое:",
"value": [], "value": [],
"options": [ "options": [
@@ -1612,7 +1612,7 @@
}, },
"adult": { "adult": {
"label": "Возрастные ограничения:", "label": "Возрастные ограничения:",
"value": "0", "value": "1",
"options": [ "options": [
{ {
"label": "Все", "label": "Все",
+1 -1
View File
@@ -1,6 +1,6 @@
[ [
{ {
"id": "tl.rulate", "id": "rulate",
"sourceSite": "https://tl.rulate.ru", "sourceSite": "https://tl.rulate.ru",
"sourceName": "Rulate", "sourceName": "Rulate",
"filters": { "filters": {
+28 -68
View File
@@ -22,9 +22,8 @@ class RulatePlugin implements Plugin.PluginBase {
constructor(metadata: RulateMetadata) { constructor(metadata: RulateMetadata) {
this.id = metadata.id; this.id = metadata.id;
this.name = metadata.sourceName + "[rulate]"; this.name = metadata.sourceName;
const iconFileName = metadata.sourceName.replace(/\s+/g, "").toLowerCase(); this.icon = `multisrc/rulate/icons/${metadata.id}.png`;
this.icon = `multisrc/rulate/icons/${iconFileName}.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = "1.0.0"; this.version = "1.0.0";
this.filters = metadata.filters; this.filters = metadata.filters;
@@ -35,8 +34,7 @@ class RulatePlugin implements Plugin.PluginBase {
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions, { filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
const baseUrl = this.site; let url = this.site + "/search?t=";
let url = baseUrl + "/search?t=";
url += "&cat=" + (filters?.cat?.value || "0"); url += "&cat=" + (filters?.cat?.value || "0");
url += "&s_lang=" + (filters?.s_lang?.value || "0"); url += "&s_lang=" + (filters?.s_lang?.value || "0");
url += "&t_lang=" + (filters?.t_lang?.value || "0"); url += "&t_lang=" + (filters?.t_lang?.value || "0");
@@ -45,39 +43,13 @@ class RulatePlugin implements Plugin.PluginBase {
url += "&atmosphere=" + (filters?.atmosphere?.value || "0"); url += "&atmosphere=" + (filters?.atmosphere?.value || "0");
url += "&adult=" + (filters?.adult?.value || "0"); url += "&adult=" + (filters?.adult?.value || "0");
if (filters?.genres?.value instanceof Array) { Object.entries(filters || {}).forEach(([type, { value }]) => {
url += filters.genres.value.map((i) => "&genres[]=" + i).join(""); if (value instanceof Array && value.length) {
} url += "&" + value
.map((val) => (type == "extra" ? val + "=1" : type + "[]=" + val))
//if (filters?.genres?.value?.exclude instanceof Array) { .join("&");
// url += filters.genres.value.exclude }
// ?.map((i) => "&genres_ex[]=" + i) });
// .join("");
//}
if (filters?.tags?.value instanceof Array) {
url += filters.tags.value.map((i) => "&tags[]=" + i).join("");
}
//if (filters?.tags?.value?.exclude instanceof Array) {
// url += filters.tags.value.exclude
// ?.map((i) => "&tags_ex[]=" + i)
// .join("");
//}
if (filters?.fandoms?.value instanceof Array) {
url += filters.fandoms.value.map((i) => "&fandoms[]=" + i).join("");
}
// if (filters?.fandoms?.value?.exclude instanceof Array) {
// url += filters.fandoms.value.exclude
// .map((i) => "&fandoms_ex[]=" + i)
// .join("");
// }
if (filters?.trash?.value instanceof Array) {
url += filters.trash.value.map((i) => "&" + i + "=1").join("");
}
url += "&Book_page=" + pageNo; url += "&Book_page=" + pageNo;
@@ -86,21 +58,20 @@ class RulatePlugin implements Plugin.PluginBase {
loadedCheerio( loadedCheerio(
'ul[class="search-results"] > li:not([class="ad_type_catalog"])', 'ul[class="search-results"] > li:not([class="ad_type_catalog"])',
).each(function () { ).each((index, element) => {
loadedCheerio(this).find("p > a").text(); loadedCheerio(element).find("p > a").text();
const name = loadedCheerio(this).find("p > a").text(); const name = loadedCheerio(element).find("p > a").text();
const cover = loadedCheerio(this).find("img").attr("src"); const cover = loadedCheerio(element).find("img").attr("src");
const url = loadedCheerio(this).find("p > a").attr("href"); const url = loadedCheerio(element).find("p > a").attr("href");
if (!name || !url) return; if (!name || !url) return;
novels.push({ name, cover: baseUrl + cover, url: baseUrl + url }); novels.push({ name, cover: this.site + cover, url: this.site + url });
}); });
return novels; return novels;
} }
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> { async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const baseUrl = this.site;
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
url: novelUrl, url: novelUrl,
}; };
@@ -129,7 +100,7 @@ class RulatePlugin implements Plugin.PluginBase {
novel.name = novel.name.split("[")[0].trim(); novel.name = novel.name.split("[")[0].trim();
} }
novel.cover = novel.cover =
baseUrl + loadedCheerio('div[class="images"] > div img').attr("src"); this.site + loadedCheerio('div[class="images"] > div img').attr("src");
novel.summary = novel.summary =
loadedCheerio("#Info > div:nth-child(3), .book-description").text().trim(); loadedCheerio("#Info > div:nth-child(3), .book-description").text().trim();
novel.author = loadedCheerio( novel.author = loadedCheerio(
@@ -172,27 +143,17 @@ class RulatePlugin implements Plugin.PluginBase {
} }
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
loadedCheerio("table > tbody > tr.chapter_row").each(function () { loadedCheerio("table > tbody > tr.chapter_row").each((chapterIndex, element) => {
const chapterName = loadedCheerio(this) const chapterName = loadedCheerio(element).find('td[class="t"] > a').text().trim();
.find('td[class="t"] > a') const releaseDate = loadedCheerio(element).find("td > span").attr("title")?.trim();
.text() const chapterUrl = loadedCheerio(element).find('td[class="t"] > a').attr("href");
.trim();
const releaseDate = loadedCheerio(this)
.find("td > span")
.attr("title")
?.trim();
const chapterUrl = loadedCheerio(this)
.find('td[class="t"] > a')
.attr("href");
if ( if (!loadedCheerio(element).find('td > span[class="disabled"]').length && releaseDate) {
!loadedCheerio(this).find('td > span[class="disabled"]').length &&
releaseDate
) {
chapters.push({ chapters.push({
name: chapterName, name: chapterName,
url: this.site + chapterUrl,
releaseTime: releaseDate, releaseTime: releaseDate,
url: baseUrl + chapterUrl, chapterNumber: chapterIndex + 1,
}); });
} }
}); });
@@ -202,7 +163,6 @@ class RulatePlugin implements Plugin.PluginBase {
} }
async parseChapter(chapterUrl: string): Promise<string> { async parseChapter(chapterUrl: string): Promise<string> {
const baseUrl = this.site;
let result = await fetchApi(chapterUrl); let result = await fetchApi(chapterUrl);
if (result.url.includes("mature?path=")) { if (result.url.includes("mature?path=")) {
const formData = new FormData(); const formData = new FormData();
@@ -218,10 +178,10 @@ class RulatePlugin implements Plugin.PluginBase {
const body = await result.text(); const body = await result.text();
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
loadedCheerio(".content-text img").each(function () { loadedCheerio(".content-text img").each((index, element) => {
if (!loadedCheerio(this).attr("src")?.startsWith("http")) { if (!loadedCheerio(element).attr("src")?.startsWith("http")) {
const src = loadedCheerio(this).attr("src"); const src = loadedCheerio(element).attr("src");
loadedCheerio(this).attr("src", baseUrl + src); loadedCheerio(element).attr("src", this.site + src);
} }
}); });
+7 -3
View File
@@ -25,7 +25,7 @@ class WPmangaStreamPlugin implements Plugin.PluginBase {
constructor(metadata: WPmangaStreamMetadata) { constructor(metadata: WPmangaStreamMetadata) {
this.id = metadata.id; this.id = metadata.id;
this.name = metadata.sourceName + "[wpmangastream]"; this.name = metadata.sourceName;
this.icon = `multisrc/wpmangastream/icons/${metadata.id}.png`; this.icon = `multisrc/wpmangastream/icons/${metadata.id}.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = "1.0.0"; this.version = "1.0.0";
@@ -93,13 +93,17 @@ class WPmangaStreamPlugin implements Plugin.PluginBase {
novel.summary += loadedCheerio(p).text().trim() + "\\n\\n"; novel.summary += loadedCheerio(p).text().trim() + "\\n\\n";
} }
const totalChapters = loadedCheerio(".eplister li").length;
const chapters: Plugin.ChapterItem[] = loadedCheerio(".eplister li") const chapters: Plugin.ChapterItem[] = loadedCheerio(".eplister li")
.map((index, element) => ({ .map((chapterIndex, element) => ({
name: name:
loadedCheerio(element).find(".epl-num").text() + loadedCheerio(element).find(".epl-num").text() +
loadedCheerio(element).find(".epl-title").text(), loadedCheerio(element).find(".epl-title").text(),
releaseTime: loadedCheerio(element).find(".epl-date").text().trim(),
url: loadedCheerio(element).find("a").attr("href") || "", url: loadedCheerio(element).find("a").attr("href") || "",
releaseTime: loadedCheerio(element).find(".epl-date").text().trim(),
chapterNumber: this.options?.reverseChapters
? totalChapters - chapterIndex
: chapterIndex + 1,
})) }))
.get() .get()
.filter((chapter) => chapter.name && chapter.url); .filter((chapter) => chapter.name && chapter.url);
+1 -1
View File
@@ -31,7 +31,7 @@ export const all_plugins = (): PluginList => {
const { id, name, version, icon } = instance; const { id, name, version, icon } = instance;
const info: Plugin.PluginItem = { const info: Plugin.PluginItem = {
id, id,
name, name: name + (plugin.match(/(\[.+\])/)?.[1] || ''),
lang: languageNative, lang: languageNative,
version, version,
requirePath, requirePath,