Plugins (#718)
* fix * test * sync * nitromanga -> madara * test wuxiaworld * xm * add chapterNumber * fix chapterNumber > 0 * test filters * nice * tunovelaligera -> madara * Squashed commit of the following: commit4c7bee5855Author: nyagami <hoangquan05112002@gmail.com> Date: Mon Jan 29 17:56:23 2024 +0700 fix: use english lang in plugin item consistent commitb26d1129deAuthor: 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 commit6ff206eab3Author: 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 commit62fc988019Author: Marcin Majewski <skilletssgames@gmail.com> Date: Fri Jan 26 14:42:54 2024 +0100 Fix search box styles (#727) commit3a1e0c43ebAuthor: 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: commitcadf7a72feAuthor: Felipe Bonfim <felipebonfim2006@gmail.com> Date: Tue Jan 30 14:55:34 2024 -0400 kiniga added (#739) * kiniga added * kiniga updated icon commit38fc1860daAuthor: 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 commit4c7bee5855Author: nyagami <hoangquan05112002@gmail.com> Date: Mon Jan 29 17:56:23 2024 +0700 fix: use english lang in plugin item consistent commitb26d1129deAuthor: 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 commit6ff206eab3Author: 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 commit62fc988019Author: Marcin Majewski <skilletssgames@gmail.com> Date: Fri Jan 26 14:42:54 2024 +0100 Fix search box styles (#727) commit3a1e0c43ebAuthor: 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:
@@ -2,7 +2,7 @@ import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { parseMadaraDate } from "@libs/parseMadaraDate";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class ComradeMaoPlugin implements Plugin.PluginBase {
|
||||
id = "comrademao";
|
||||
@@ -20,14 +20,14 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
|
||||
const novelName = loadedCheerio(this).find(".tt").text().trim();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
}
|
||||
};
|
||||
novels.push(novel);
|
||||
});
|
||||
return novels;
|
||||
@@ -35,7 +35,7 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site;
|
||||
|
||||
@@ -43,25 +43,19 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
|
||||
|
||||
link += "page/" + pageNo + "/?post_type=novel";
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
const body = await fetchApi(link).then((result) => result.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
const body = await fetchApi(novelUrl).then((result) => result.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
url: novelUrl,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
@@ -69,23 +63,23 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
|
||||
|
||||
novel.cover = loadedCheerio("div.thumbook > div > img").attr("src");
|
||||
|
||||
loadedCheerio(".infox .wd-full").each((i,el) => {
|
||||
const detailName = loadedCheerio(el).find("b").text();
|
||||
const detail = loadedCheerio(el).find('span').text();
|
||||
loadedCheerio(".infox .wd-full").each((i, el) => {
|
||||
const detailName = loadedCheerio(el).find("b").text();
|
||||
const detail = loadedCheerio(el).find("span").text();
|
||||
|
||||
switch (detailName) {
|
||||
case 'Publisher: ':
|
||||
novel.author = detail.trim();
|
||||
break;
|
||||
case 'Status: ':
|
||||
novel.status = detail.trim();
|
||||
break;
|
||||
case 'Genres:':
|
||||
novel.genres = detail.trim().replace(/ , /g, ',');
|
||||
break;
|
||||
case 'Description: ':
|
||||
novel.summary = detail.trim();
|
||||
break;
|
||||
case "Publisher: ":
|
||||
novel.author = detail.trim();
|
||||
break;
|
||||
case "Status: ":
|
||||
novel.status = detail.trim();
|
||||
break;
|
||||
case "Genres:":
|
||||
novel.genres = detail.trim().replace(/ , /g, ",");
|
||||
break;
|
||||
case "Description: ":
|
||||
novel.summary = detail.trim();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,53 +88,78 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
|
||||
loadedCheerio("#chapterlist")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const releaseDate = parseMadaraDate(
|
||||
loadedCheerio(this).find(".chapterdate").text()
|
||||
);
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
if (!chapterUrl) return;
|
||||
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find(".chapternum")
|
||||
.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({
|
||||
name: chapterName,
|
||||
url: chapterUrl,
|
||||
releaseTime: releaseDate,
|
||||
releaseTime,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const body = await fetchApi(chapterUrl).then((result) => result.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || "";
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number
|
||||
pageNo: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
|
||||
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
const body = await fetchApi(url).then((result) => result.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
};
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
|
||||
@@ -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
@@ -1,103 +1,108 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class FreeWebNovel implements Plugin.PluginBase {
|
||||
id = "FWN.com";
|
||||
name = "Web NOVEL";
|
||||
site = "https://freewebnovel.com";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/freewebnovel/icon.png";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const sort = showLatestNovels
|
||||
? "/latest-release-novels/"
|
||||
: "/completed-novels/";
|
||||
|
||||
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) => ({
|
||||
name: loadedCheerio(element).find(".tit").text(),
|
||||
cover: loadedCheerio(element).find("img").attr("src"),
|
||||
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
|
||||
}))
|
||||
.get();
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(url: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = { url };
|
||||
novel.name = loadedCheerio("h1.tit").text();
|
||||
novel.cover = loadedCheerio(".pic > img").attr("src");
|
||||
novel.genres = loadedCheerio("[title=Genre]")
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.author = loadedCheerio("[title=Author]")
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.status = loadedCheerio("[title=Status]")
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.summary = loadedCheerio(".inner").text().trim();
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a")
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).attr("title") || "Chapter " + index,
|
||||
releaseTime: null,
|
||||
url: this.site + loadedCheerio(element).attr("href"),
|
||||
}))
|
||||
.get();
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("div.txt").html() || "";
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchkey: string,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const body = await fetchApi(this.site + "/search/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ searchkey }),
|
||||
}).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con")
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find(".tit").text(),
|
||||
cover: loadedCheerio(element).find(".pic > a > img").attr("data-cfsrc"),
|
||||
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
|
||||
}))
|
||||
.get();
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
}
|
||||
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class FreeWebNovel implements Plugin.PluginBase {
|
||||
id = "FWN.com";
|
||||
name = "Web NOVEL";
|
||||
site = "https://freewebnovel.com";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/freewebnovel/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const sort = showLatestNovels
|
||||
? "/latest-release-novels/"
|
||||
: "/completed-novels/";
|
||||
|
||||
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) => ({
|
||||
name: loadedCheerio(element).find(".tit").text(),
|
||||
cover: loadedCheerio(element).find("img").attr("src"),
|
||||
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
|
||||
}))
|
||||
.get();
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(url: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = { url };
|
||||
novel.name = loadedCheerio("h1.tit").text();
|
||||
novel.cover = loadedCheerio(".pic > img").attr("src");
|
||||
novel.genres = loadedCheerio("[title=Genre]")
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.author = loadedCheerio("[title=Author]")
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.status = loadedCheerio("[title=Status]")
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.summary = loadedCheerio(".inner").text().trim();
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a")
|
||||
.map((chapterIndex, element) => ({
|
||||
name: loadedCheerio(element).attr("title") || "Chapter " + (chapterIndex + 1),
|
||||
url: this.site + loadedCheerio(element).attr("href"),
|
||||
releaseTime: null,
|
||||
chapterNumber: chapterIndex + 1,
|
||||
}))
|
||||
.get();
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("div.txt").html() || "";
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const body = await fetchApi(this.site + "/search/", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Referer: this.site,
|
||||
Origin: this.site,
|
||||
},
|
||||
method: "POST",
|
||||
body: "searchkey=" + encodeURIComponent(searchTerm)
|
||||
}).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con")
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find(".tit").text(),
|
||||
cover: loadedCheerio(element).find(".pic > a > img").attr("src"),
|
||||
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
|
||||
}))
|
||||
.get();
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
}
|
||||
|
||||
export default new FreeWebNovel();
|
||||
|
||||
@@ -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();
|
||||
@@ -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
@@ -1,241 +1,242 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class freedlit implements Plugin.PluginBase {
|
||||
id = "freedlit.space";
|
||||
name = "LitSpace";
|
||||
site = "https://freedlit.space";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/freedlit/icon.png";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/get-books/all/list/" + page + "?sort=";
|
||||
url += showLatestNovels ? "recent" : filters?.sort?.value || "popular";
|
||||
url += "&status=" + (filters?.status?.value || "all");
|
||||
url += "&access=" + (filters?.access?.value || "all");
|
||||
url += "&adult=" + (filters?.adult?.value || "hide");
|
||||
|
||||
if (filters?.genre?.value?.include?.length) {
|
||||
url += filters.genre.value.include
|
||||
.map(id => '&genres_included[]=' + id).join("");
|
||||
}
|
||||
|
||||
if (filters?.genre?.value?.exclude?.length) {
|
||||
url += filters.genre.value.exclude
|
||||
.map(id => '&genres_excluded[]=' + id).join("");
|
||||
}
|
||||
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("#bookListBlock > div > div").each(function () {
|
||||
const name = loadedCheerio(this).find("div > h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(this)
|
||||
.find("div > a > img")
|
||||
.attr("src")
|
||||
?.trim();
|
||||
const url = loadedCheerio(this).find("div > h4 > a").attr("href")?.trim();
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: loadedCheerio(".book-info h4").text(),
|
||||
cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(),
|
||||
summary: loadedCheerio("#nav-home").text()?.trim(),
|
||||
author: loadedCheerio(".book-info h5 > a").text(),
|
||||
genres: loadedCheerio(".genre-list > a")
|
||||
.map((index, element) => loadedCheerio(element).text())
|
||||
.get()
|
||||
.join(","),
|
||||
};
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("a.chapter-line").each(function () {
|
||||
const name = loadedCheerio(this).find("h6").text();
|
||||
const releaseTime = loadedCheerio(this).find('span[class="date"]').text();
|
||||
const url = loadedCheerio(this).attr("href");
|
||||
if (!name || !url) return;
|
||||
|
||||
chapters.push({ name, releaseTime, url });
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("div.mobile-block").remove();
|
||||
loadedCheerio("div.standart-block").remove();
|
||||
|
||||
const chapterText = loadedCheerio('div[class="chapter"]').html() || "";
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/search?query=${searchTerm}&type=all`;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("#bookListBlock > div").each(function () {
|
||||
const name = loadedCheerio(this).find("h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(this).find("a > img").attr("src")?.trim();
|
||||
const url = loadedCheerio(this).find("h4 > a").attr("href")?.trim();
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка:",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "По популярности", value: "popular" },
|
||||
{ label: "По количеству комментариев", value: "comments" },
|
||||
{ label: "По количеству лайков", value: "likes" },
|
||||
{ label: "По новизне", value: "recent" },
|
||||
{ label: "По просмотрам", value: "views" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
label: "Жанры:",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "Любой жанр", value: "all" },
|
||||
{ label: "Альтернативная история", value: "alternative-history" },
|
||||
{ label: "Антиутопия", value: "dystopia" },
|
||||
{ label: "Бизнес-литература", value: "business-literature" },
|
||||
{ label: "Боевая фантастика", value: "combat-fiction" },
|
||||
{ label: "Боевик", value: "action" },
|
||||
{ label: "Боевое фэнтези", value: "combat-fantasy" },
|
||||
{ label: "Бояръ-Аниме", value: "boyar-anime" },
|
||||
{ label: "Героическая фантастика", value: "heroic-fiction" },
|
||||
{ label: "Героическое фэнтези", value: "heroic-fantasy" },
|
||||
{ label: "Городское фэнтези", value: "urban-fantasy" },
|
||||
{ label: "Гримдарк", value: "grimdark" },
|
||||
{ label: "Детектив", value: "mystery" },
|
||||
{ label: "Детская литература", value: "kids-literature" },
|
||||
{ label: "Документальная проза", value: "biography" },
|
||||
{ label: "Историческая проза", value: "historical-fiction" },
|
||||
{ label: "Исторический детектив", value: "historical-mystery" },
|
||||
{ label: "Исторический любовный роман", value: "historical-romantic-novel" },
|
||||
{ label: "Историческое фэнтези", value: "historical-fantasy" },
|
||||
{ label: "Киберпанк", value: "cyberpunk" },
|
||||
{ label: "Космическая фантастика", value: "cosmic-fiction" },
|
||||
{ label: "ЛитРПГ", value: "litrpg" },
|
||||
{ label: "Лоу / Низкое фэнтези", value: "low-fantasy" },
|
||||
{ label: "Любовное фэнтези", value: "romantic-fantasy" },
|
||||
{ label: "Любовный роман", value: "romantic-novel" },
|
||||
{ label: "Мистика", value: "mystic" },
|
||||
{ label: "Мистический детектив", value: "mystic-detective" },
|
||||
{ label: "Научная фантастика", value: "science-fiction" },
|
||||
{ label: "Подростковая проза", value: "young-adult" },
|
||||
{ label: "Политический роман", value: "political-romance" },
|
||||
{ label: "Попаданцы", value: "accidental-travel" },
|
||||
{ label: "Попаданцы в магические миры", value: "magic-worlds-travel" },
|
||||
{ label: "Попаданцы во времени", value: "time-travel" },
|
||||
{ label: "Порнотика", value: "pornotica" },
|
||||
{ label: "Постапокалипсис", value: "post-apocalypse" },
|
||||
{ label: "Поэзия", value: "poetry" },
|
||||
{ label: "Приключения", value: "adventure" },
|
||||
{ label: "Публицистика", value: "journalism" },
|
||||
{ label: "Пьеса", value: "play" },
|
||||
{ label: "Развитие личности", value: "how-to-book" },
|
||||
{ label: "Разное", value: "other" },
|
||||
{ label: "Реализм", value: "Realism" },
|
||||
{ label: "РеалРПГ", value: "realrpg" },
|
||||
{ label: "Репликация", value: "replication" },
|
||||
{ label: "Романтическая эротика", value: "romantic-erotic-fiction" },
|
||||
{ label: "Сказка", value: "fairy-tale" },
|
||||
{ label: "Слэш", value: "slash" },
|
||||
{ label: "Современная проза", value: "modern-prose" },
|
||||
{ label: "Современный любовный роман", value: "modern-romantic-novel" },
|
||||
{ label: "Социальная фантастика", value: "social-fiction" },
|
||||
{ label: "Стимпанк", value: "steampunk" },
|
||||
{ label: "Сценарий", value: "scenario" },
|
||||
{ label: "Сюаньхуань", value: "xuanhuan" },
|
||||
{ label: "Сянься", value: "xianxia" },
|
||||
{ label: "Тёмное фэнтези", value: "dark-fantasy" },
|
||||
{ label: "Триллер", value: "thriller" },
|
||||
{ label: "Уся", value: "wuxia" },
|
||||
{ label: "Фантастика", value: "fiction" },
|
||||
{ label: "Фантастический детектив", value: "mystery-fiction" },
|
||||
{ label: "Фанфик", value: "fan-fiction" },
|
||||
{ label: "Фемслэш", value: "femslash" },
|
||||
{ label: "Фэнтези", value: "fantasy" },
|
||||
{ label: "Хоррор", value: "horror" },
|
||||
{ label: "Шпионский детектив", value: "spy-crime" },
|
||||
{ label: "Эпическое фэнтези", value: "epic-fantasy" },
|
||||
{ label: "Эротика", value: "erotic-fiction" },
|
||||
{ label: "Эротическая фантастика", value: "erotic-fiction" },
|
||||
{ label: "Эротический фанфик", value: "erotic-fan-fiction" },
|
||||
{ label: "Эротическое фэнтези", value: "erotic-fantasy" },
|
||||
{ label: "Этническое фэнтези", value: "ethnic-fantasy" },
|
||||
{ label: "Юмор", value: "humor" },
|
||||
{ label: "Юмористическая фантастика", value: "humor-fiction" },
|
||||
{ label: "Юмористическое фэнтези", value: "humor-fantasy" },
|
||||
{ label: "RPS", value: "rps" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
status: {
|
||||
label: "Статус:",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Любой статус", value: "all" },
|
||||
{ label: "В процессе", value: "in-process" },
|
||||
{ label: "Завершено", value: "finished" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
access: {
|
||||
label: "Доступ:",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Любой доступ", value: "all" },
|
||||
{ label: "Бесплатные", value: "free" },
|
||||
{ label: "Платные", value: "paid" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
adult: {
|
||||
label: "Возрастные ограничения:",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Скрыть 18+", value: "hide" },
|
||||
{ label: "Показать +18", value: "show" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class freedlit implements Plugin.PluginBase {
|
||||
id = "freedlit.space";
|
||||
name = "LitSpace";
|
||||
site = "https://freedlit.space";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/freedlit/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/get-books/all/list/" + page + "?sort=";
|
||||
url += showLatestNovels ? "recent" : filters?.sort?.value || "popular";
|
||||
url += "&status=" + (filters?.status?.value || "all");
|
||||
url += "&access=" + (filters?.access?.value || "all");
|
||||
url += "&adult=" + (filters?.adult?.value || "hide");
|
||||
|
||||
if (filters?.genre?.value?.include?.length) {
|
||||
url += filters.genre.value.include
|
||||
.map((id) => "&genres_included[]=" + id)
|
||||
.join("");
|
||||
}
|
||||
|
||||
if (filters?.genre?.value?.exclude?.length) {
|
||||
url += filters.genre.value.exclude
|
||||
.map((id) => "&genres_excluded[]=" + id)
|
||||
.join("");
|
||||
}
|
||||
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("#bookListBlock > div > div").each(function () {
|
||||
const name = loadedCheerio(this).find("div > h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(this)
|
||||
.find("div > a > img")
|
||||
.attr("src")
|
||||
?.trim();
|
||||
const url = loadedCheerio(this).find("div > h4 > a").attr("href")?.trim();
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: loadedCheerio(".book-info h4").text(),
|
||||
cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(),
|
||||
summary: loadedCheerio("#nav-home").text()?.trim(),
|
||||
author: loadedCheerio(".book-info h5 > a").text(),
|
||||
genres: loadedCheerio(".genre-list > a")
|
||||
.map((index, element) => loadedCheerio(element).text())
|
||||
.get()
|
||||
.join(","),
|
||||
};
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("a.chapter-line").each((chapterIndex, element) => {
|
||||
const name = loadedCheerio(element).find("h6").text();
|
||||
const url = loadedCheerio(element).attr("href");
|
||||
if (!name || !url) return;
|
||||
|
||||
const releaseTime = loadedCheerio(element).find('span[class="date"]').text();
|
||||
chapters.push({ name, url, releaseTime, chapterNumber: chapterIndex + 1 });
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("div.mobile-block").remove();
|
||||
loadedCheerio("div.standart-block").remove();
|
||||
|
||||
const chapterText = loadedCheerio('div[class="chapter"]').html() || "";
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/search?query=${searchTerm}&type=all`;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("#bookListBlock > div").each(function () {
|
||||
const name = loadedCheerio(this).find("h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(this).find("a > img").attr("src")?.trim();
|
||||
const url = loadedCheerio(this).find("h4 > a").attr("href")?.trim();
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка:",
|
||||
value: "popular",
|
||||
options: [
|
||||
{ label: "По популярности", value: "popular" },
|
||||
{ label: "По количеству комментариев", value: "comments" },
|
||||
{ label: "По количеству лайков", value: "likes" },
|
||||
{ label: "По новизне", value: "recent" },
|
||||
{ label: "По просмотрам", value: "views" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
label: "Жанры:",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "Любой жанр", value: "all" },
|
||||
{ label: "Альтернативная история", value: "alternative-history" },
|
||||
{ label: "Антиутопия", value: "dystopia" },
|
||||
{ label: "Бизнес-литература", value: "business-literature" },
|
||||
{ label: "Боевая фантастика", value: "combat-fiction" },
|
||||
{ label: "Боевик", value: "action" },
|
||||
{ label: "Боевое фэнтези", value: "combat-fantasy" },
|
||||
{ label: "Бояръ-Аниме", value: "boyar-anime" },
|
||||
{ label: "Героическая фантастика", value: "heroic-fiction" },
|
||||
{ label: "Героическое фэнтези", value: "heroic-fantasy" },
|
||||
{ label: "Городское фэнтези", value: "urban-fantasy" },
|
||||
{ label: "Гримдарк", value: "grimdark" },
|
||||
{ label: "Детектив", value: "mystery" },
|
||||
{ label: "Детская литература", value: "kids-literature" },
|
||||
{ label: "Документальная проза", value: "biography" },
|
||||
{ label: "Историческая проза", value: "historical-fiction" },
|
||||
{ label: "Исторический детектив", value: "historical-mystery" },
|
||||
{ label: "Исторический любовный роман", value: "historical-romantic-novel" },
|
||||
{ label: "Историческое фэнтези", value: "historical-fantasy" },
|
||||
{ label: "Киберпанк", value: "cyberpunk" },
|
||||
{ label: "Космическая фантастика", value: "cosmic-fiction" },
|
||||
{ label: "ЛитРПГ", value: "litrpg" },
|
||||
{ label: "Лоу / Низкое фэнтези", value: "low-fantasy" },
|
||||
{ label: "Любовное фэнтези", value: "romantic-fantasy" },
|
||||
{ label: "Любовный роман", value: "romantic-novel" },
|
||||
{ label: "Мистика", value: "mystic" },
|
||||
{ label: "Мистический детектив", value: "mystic-detective" },
|
||||
{ label: "Научная фантастика", value: "science-fiction" },
|
||||
{ label: "Подростковая проза", value: "young-adult" },
|
||||
{ label: "Политический роман", value: "political-romance" },
|
||||
{ label: "Попаданцы", value: "accidental-travel" },
|
||||
{ label: "Попаданцы в магические миры", value: "magic-worlds-travel" },
|
||||
{ label: "Попаданцы во времени", value: "time-travel" },
|
||||
{ label: "Порнотика", value: "pornotica" },
|
||||
{ label: "Постапокалипсис", value: "post-apocalypse" },
|
||||
{ label: "Поэзия", value: "poetry" },
|
||||
{ label: "Приключения", value: "adventure" },
|
||||
{ label: "Публицистика", value: "journalism" },
|
||||
{ label: "Пьеса", value: "play" },
|
||||
{ label: "Развитие личности", value: "how-to-book" },
|
||||
{ label: "Разное", value: "other" },
|
||||
{ label: "Реализм", value: "Realism" },
|
||||
{ label: "РеалРПГ", value: "realrpg" },
|
||||
{ label: "Репликация", value: "replication" },
|
||||
{ label: "Романтическая эротика", value: "romantic-erotic-fiction" },
|
||||
{ label: "Сказка", value: "fairy-tale" },
|
||||
{ label: "Слэш", value: "slash" },
|
||||
{ label: "Современная проза", value: "modern-prose" },
|
||||
{ label: "Современный любовный роман", value: "modern-romantic-novel" },
|
||||
{ label: "Социальная фантастика", value: "social-fiction" },
|
||||
{ label: "Стимпанк", value: "steampunk" },
|
||||
{ label: "Сценарий", value: "scenario" },
|
||||
{ label: "Сюаньхуань", value: "xuanhuan" },
|
||||
{ label: "Сянься", value: "xianxia" },
|
||||
{ label: "Тёмное фэнтези", value: "dark-fantasy" },
|
||||
{ label: "Триллер", value: "thriller" },
|
||||
{ label: "Уся", value: "wuxia" },
|
||||
{ label: "Фантастика", value: "fiction" },
|
||||
{ label: "Фантастический детектив", value: "mystery-fiction" },
|
||||
{ label: "Фанфик", value: "fan-fiction" },
|
||||
{ label: "Фемслэш", value: "femslash" },
|
||||
{ label: "Фэнтези", value: "fantasy" },
|
||||
{ label: "Хоррор", value: "horror" },
|
||||
{ label: "Шпионский детектив", value: "spy-crime" },
|
||||
{ label: "Эпическое фэнтези", value: "epic-fantasy" },
|
||||
{ label: "Эротика", value: "erotic-fiction" },
|
||||
{ label: "Эротическая фантастика", value: "erotic-fiction" },
|
||||
{ label: "Эротический фанфик", value: "erotic-fan-fiction" },
|
||||
{ label: "Эротическое фэнтези", value: "erotic-fantasy" },
|
||||
{ label: "Этническое фэнтези", value: "ethnic-fantasy" },
|
||||
{ label: "Юмор", value: "humor" },
|
||||
{ label: "Юмористическая фантастика", value: "humor-fiction" },
|
||||
{ label: "Юмористическое фэнтези", value: "humor-fantasy" },
|
||||
{ label: "RPS", value: "rps" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
status: {
|
||||
label: "Статус:",
|
||||
value: "all",
|
||||
options: [
|
||||
{ label: "Любой статус", value: "all" },
|
||||
{ label: "В процессе", value: "in-process" },
|
||||
{ label: "Завершено", value: "finished" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
access: {
|
||||
label: "Доступ:",
|
||||
value: "all",
|
||||
options: [
|
||||
{ label: "Любой доступ", value: "all" },
|
||||
{ label: "Бесплатные", value: "free" },
|
||||
{ label: "Платные", value: "paid" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
adult: {
|
||||
label: "Возрастные ограничения:",
|
||||
value: "hide",
|
||||
options: [
|
||||
{ label: "Скрыть 18+", value: "hide" },
|
||||
{ label: "Показать +18", value: "show" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new freedlit();
|
||||
|
||||
+529
-538
File diff suppressed because it is too large
Load Diff
+252
-258
@@ -1,258 +1,252 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class Bookriver implements Plugin.PluginBase {
|
||||
id = "bookriver";
|
||||
name = "Bookriver";
|
||||
site = "https://bookriver.ru";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/bookriver/icon.png";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + `/genre?page=${pageNo}&perPage=24&sortingType=`;
|
||||
url += showLatestNovels
|
||||
? "last-update"
|
||||
: filters?.sort?.value || "bestseller";
|
||||
|
||||
if (filters?.genres?.value instanceof Array && filters?.genres?.value.length) {
|
||||
url += "&g=" + filters.genres.value.join(",");
|
||||
}
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
if (jsonRaw) {
|
||||
const json: response = JSON.parse(jsonRaw);
|
||||
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: novel.coverImages[0].url,
|
||||
url: this.site + "/book/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const json: response = JSON.parse(jsonRaw || "{}");
|
||||
const book = json.props.pageProps.state.book?.bookPage;
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.name,
|
||||
cover: book?.coverImages[0].url,
|
||||
summary: book?.annotation,
|
||||
author: book?.author?.name,
|
||||
genres: book?.tags?.map((item) => item.name).join(", "),
|
||||
status:
|
||||
book?.statusComplete === "writing"
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
};
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
book?.ebook?.chapters?.forEach((chapter) => {
|
||||
if (chapter.available) {
|
||||
chapters.push({
|
||||
name: chapter.name,
|
||||
releaseTime: dayjs(
|
||||
chapter?.firstPublishedAt || chapter.createdAt,
|
||||
).format("LLL"),
|
||||
url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId,
|
||||
});
|
||||
}
|
||||
});
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
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());
|
||||
const json = (await result.json()) as responseChapter;
|
||||
|
||||
let chapterText = json.data.content || "Конец произведения";
|
||||
if (json.data?.audio?.available) {
|
||||
chapterText += "\n" + json.data.audio.url;
|
||||
}
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `https://api.bookriver.ru/api/v1/search/autocomplete?keyword=${searchTerm}&page=${pageNo}&perPage=10`;
|
||||
const result = await fetchApi(url);
|
||||
const json = (await result.json()) as responseSearch;
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
json?.data?.books?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: novel.coverImages[0].url,
|
||||
url: this.site + "/book/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Бестселлеры", value: "bestseller" },
|
||||
{ label: "Дате добавления", value: "newest" },
|
||||
{ label: "Дате обновления", value: "last-update" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
label: "жанры",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Альтернативная история", value: "alternativnaya-istoriya" },
|
||||
{ label: "Боевая фантастика", value: "boevaya-fantastika" },
|
||||
{ label: "Боевое фэнтези", value: "boevoe-fentezi" },
|
||||
{ label: "Бытовое фэнтези", value: "bytovoe-fentezi" },
|
||||
{ label: "Героическая фантастика", value: "geroicheskaya-fantastika" },
|
||||
{ label: "Героическое фэнтези", value: "geroicheskoe-fentezi" },
|
||||
{ label: "Городское фэнтези", value: "gorodskoe-fentezi" },
|
||||
{ label: "Детектив", value: "detektiv" },
|
||||
{ label: "Детективная фантастика", value: "detektivnaya-fantastika" },
|
||||
{ label: "Жёсткая эротика", value: "zhyostkaya-erotika" },
|
||||
{ label: "Исторический детектив", value: "istoricheskii-detektiv" },
|
||||
{ label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" },
|
||||
{ label: "Историческое фэнтези", value: "istoricheskoe-fentezi" },
|
||||
{ label: "Киберпанк", value: "kiberpank" },
|
||||
{ label: "Классический детектив", value: "klassicheskii-detektiv" },
|
||||
{ label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" },
|
||||
{ label: "Космическая фантастика", value: "kosmicheskaya-fantastika" },
|
||||
{ label: "Криминальный детектив", value: "kriminalnyi-detektiv" },
|
||||
{ label: "ЛитРПГ", value: "litrpg" },
|
||||
{ label: "Любовная фантастика", value: "lyubovnaya-fantastika" },
|
||||
{ label: "Любовное фэнтези", value: "lyubovnoe-fentezi" },
|
||||
{ label: "Любовный роман", value: "lyubovnyi-roman" },
|
||||
{ label: "Мистика", value: "mistika" },
|
||||
{ label: "Молодежная проза", value: "molodezhnaya-proza" },
|
||||
{ label: "Научная фантастика", value: "nauchnaya-fantastika" },
|
||||
{ label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" },
|
||||
{ label: "Политический детектив", value: "politicheskii-detektiv" },
|
||||
{ label: "Попаданцы", value: "popadantsy" },
|
||||
{ label: "Постапокалипсис", value: "postapokalipsis" },
|
||||
{ label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" },
|
||||
{ label: "Романтическая эротика", value: "romanticheskaya-erotika" },
|
||||
{ label: "С элементами эротики", value: "s-elementami-erotiki" },
|
||||
{ label: "Славянское фэнтези", value: "slavyanskoe-fentezi" },
|
||||
{ label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" },
|
||||
{ label: "Социальная фантастика", value: "sotsialnaya-fantastika" },
|
||||
{ label: "Тёмное фэнтези", value: "temnoe-fentezi" },
|
||||
{ label: "Фантастика", value: "fantastika" },
|
||||
{ label: "Фэнтези", value: "fentezi" },
|
||||
{ label: "Шпионский детектив", value: "shpionskii-detektiv" },
|
||||
{ label: "Эпическое фэнтези", value: "epicheskoe-fentezi" },
|
||||
{ label: "Эротика", value: "erotika" },
|
||||
{ label: "Эротическая фантастика", value: "eroticheskaya-fantastika" },
|
||||
{ label: "Эротический фанфик", value: "eroticheskii-fanfik" },
|
||||
{ label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" },
|
||||
{ label: "Юмористический детектив", value: "yumoristicheskii-detektiv" },
|
||||
{ label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
export default new Bookriver();
|
||||
|
||||
interface response {
|
||||
props: {
|
||||
pageProps: {
|
||||
state: {
|
||||
book?: {
|
||||
bookPage: BooksEntity;
|
||||
};
|
||||
pagesFilter?: {
|
||||
genre: {
|
||||
books: BooksEntity[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface BooksEntity {
|
||||
name: string;
|
||||
coverImages: { url: string }[];
|
||||
slug: string;
|
||||
annotation?: string;
|
||||
author?: {
|
||||
name: string;
|
||||
};
|
||||
tags?: { name: string }[];
|
||||
ebook?: {
|
||||
chapters: Chapter[];
|
||||
};
|
||||
statusComplete: string;
|
||||
}
|
||||
|
||||
interface Chapter {
|
||||
name: string;
|
||||
available: boolean;
|
||||
firstPublishedAt?: Date | null;
|
||||
createdAt?: Date | null;
|
||||
chapterId: number | string;
|
||||
}
|
||||
|
||||
interface responseChapter {
|
||||
data: {
|
||||
id: number;
|
||||
bookId: number;
|
||||
name: string;
|
||||
content: string;
|
||||
symbols: number;
|
||||
number: number;
|
||||
authorPages: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
firstPublishedAt: string;
|
||||
status: string;
|
||||
protected: boolean;
|
||||
free: boolean;
|
||||
audio?: {
|
||||
available: boolean;
|
||||
url: string;
|
||||
duration: number;
|
||||
};
|
||||
publicationScheduledFor?: null;
|
||||
};
|
||||
}
|
||||
|
||||
interface responseSearch {
|
||||
data: Data;
|
||||
total: number;
|
||||
}
|
||||
interface Data {
|
||||
books?: BooksEntity[] | null;
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class Bookriver implements Plugin.PluginBase {
|
||||
id = "bookriver";
|
||||
name = "Bookriver";
|
||||
site = "https://bookriver.ru";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/bookriver/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + `/genre?page=${pageNo}&perPage=24&sortingType=`;
|
||||
url += showLatestNovels ? "last-update" : filters?.sort?.value || "bestseller";
|
||||
|
||||
if (filters?.genres?.value?.length) {
|
||||
url += "&g=" + filters.genres.value.join(",");
|
||||
}
|
||||
|
||||
const result = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
if (jsonRaw) {
|
||||
const json: response = JSON.parse(jsonRaw);
|
||||
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: novel.coverImages[0].url,
|
||||
url: this.site + "/book/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const json: response = JSON.parse(jsonRaw || "{}");
|
||||
const book = json.props.pageProps.state.book?.bookPage;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.name,
|
||||
cover: book?.coverImages[0].url,
|
||||
summary: book?.annotation,
|
||||
author: book?.author?.name,
|
||||
genres: book?.tags?.map((tag) => tag.name).join(", "),
|
||||
status:
|
||||
book?.statusComplete === "writing"
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
};
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
book?.ebook?.chapters?.forEach((chapter, chapterIndex) => {
|
||||
if (chapter.available) {
|
||||
chapters.push({
|
||||
name: chapter.name,
|
||||
url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId,
|
||||
releaseTime: dayjs(chapter?.firstPublishedAt || chapter.createdAt).format("LLL"),
|
||||
chapterNumber: chapterIndex + 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
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());
|
||||
const json = (await result.json()) as responseChapter;
|
||||
|
||||
let chapterText = json.data.content || "Конец произведения";
|
||||
if (json.data?.audio?.available) {
|
||||
chapterText += "\n" + json.data.audio.url;
|
||||
}
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `https://api.bookriver.ru/api/v1/search/autocomplete?keyword=${searchTerm}&page=${pageNo}&perPage=10`;
|
||||
const result = await fetchApi(url);
|
||||
const json = (await result.json()) as responseSearch;
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
json.data?.books?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: novel.coverImages[0].url,
|
||||
url: this.site + "/book/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка",
|
||||
value: "bestseller",
|
||||
options: [
|
||||
{ label: "Бестселлеры", value: "bestseller" },
|
||||
{ label: "Дате добавления", value: "newest" },
|
||||
{ label: "Дате обновления", value: "last-update" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
label: "жанры",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Альтернативная история", value: "alternativnaya-istoriya" },
|
||||
{ label: "Боевая фантастика", value: "boevaya-fantastika" },
|
||||
{ label: "Боевое фэнтези", value: "boevoe-fentezi" },
|
||||
{ label: "Бытовое фэнтези", value: "bytovoe-fentezi" },
|
||||
{ label: "Героическая фантастика", value: "geroicheskaya-fantastika" },
|
||||
{ label: "Героическое фэнтези", value: "geroicheskoe-fentezi" },
|
||||
{ label: "Городское фэнтези", value: "gorodskoe-fentezi" },
|
||||
{ label: "Детектив", value: "detektiv" },
|
||||
{ label: "Детективная фантастика", value: "detektivnaya-fantastika" },
|
||||
{ label: "Жёсткая эротика", value: "zhyostkaya-erotika" },
|
||||
{ label: "Исторический детектив", value: "istoricheskii-detektiv" },
|
||||
{ label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" },
|
||||
{ label: "Историческое фэнтези", value: "istoricheskoe-fentezi" },
|
||||
{ label: "Киберпанк", value: "kiberpank" },
|
||||
{ label: "Классический детектив", value: "klassicheskii-detektiv" },
|
||||
{ label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" },
|
||||
{ label: "Космическая фантастика", value: "kosmicheskaya-fantastika" },
|
||||
{ label: "Криминальный детектив", value: "kriminalnyi-detektiv" },
|
||||
{ label: "ЛитРПГ", value: "litrpg" },
|
||||
{ label: "Любовная фантастика", value: "lyubovnaya-fantastika" },
|
||||
{ label: "Любовное фэнтези", value: "lyubovnoe-fentezi" },
|
||||
{ label: "Любовный роман", value: "lyubovnyi-roman" },
|
||||
{ label: "Мистика", value: "mistika" },
|
||||
{ label: "Молодежная проза", value: "molodezhnaya-proza" },
|
||||
{ label: "Научная фантастика", value: "nauchnaya-fantastika" },
|
||||
{ label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" },
|
||||
{ label: "Политический детектив", value: "politicheskii-detektiv" },
|
||||
{ label: "Попаданцы", value: "popadantsy" },
|
||||
{ label: "Постапокалипсис", value: "postapokalipsis" },
|
||||
{ label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" },
|
||||
{ label: "Романтическая эротика", value: "romanticheskaya-erotika" },
|
||||
{ label: "С элементами эротики", value: "s-elementami-erotiki" },
|
||||
{ label: "Славянское фэнтези", value: "slavyanskoe-fentezi" },
|
||||
{ label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" },
|
||||
{ label: "Социальная фантастика", value: "sotsialnaya-fantastika" },
|
||||
{ label: "Тёмное фэнтези", value: "temnoe-fentezi" },
|
||||
{ label: "Фантастика", value: "fantastika" },
|
||||
{ label: "Фэнтези", value: "fentezi" },
|
||||
{ label: "Шпионский детектив", value: "shpionskii-detektiv" },
|
||||
{ label: "Эпическое фэнтези", value: "epicheskoe-fentezi" },
|
||||
{ label: "Эротика", value: "erotika" },
|
||||
{ label: "Эротическая фантастика", value: "eroticheskaya-fantastika" },
|
||||
{ label: "Эротический фанфик", value: "eroticheskii-fanfik" },
|
||||
{ label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" },
|
||||
{ label: "Юмористический детектив", value: "yumoristicheskii-detektiv" },
|
||||
{ label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
export default new Bookriver();
|
||||
|
||||
interface response {
|
||||
props: {
|
||||
pageProps: {
|
||||
state: {
|
||||
book?: {
|
||||
bookPage: BooksEntity;
|
||||
};
|
||||
pagesFilter?: {
|
||||
genre: {
|
||||
books: BooksEntity[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface BooksEntity {
|
||||
name: string;
|
||||
coverImages: { url: string }[];
|
||||
slug: string;
|
||||
annotation?: string;
|
||||
author?: {
|
||||
name: string;
|
||||
};
|
||||
tags?: { name: string }[];
|
||||
ebook?: {
|
||||
chapters: Chapter[];
|
||||
};
|
||||
statusComplete: string;
|
||||
}
|
||||
|
||||
interface Chapter {
|
||||
name: string;
|
||||
available: boolean;
|
||||
firstPublishedAt?: Date | null;
|
||||
createdAt?: Date | null;
|
||||
chapterId: number | string;
|
||||
}
|
||||
|
||||
interface responseChapter {
|
||||
data: {
|
||||
id: number;
|
||||
bookId: number;
|
||||
name: string;
|
||||
content: string;
|
||||
symbols: number;
|
||||
number: number;
|
||||
authorPages: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
firstPublishedAt: string;
|
||||
status: string;
|
||||
protected: boolean;
|
||||
free: boolean;
|
||||
audio?: {
|
||||
available: boolean;
|
||||
url: string;
|
||||
duration: number;
|
||||
};
|
||||
publicationScheduledFor?: null;
|
||||
};
|
||||
}
|
||||
|
||||
interface responseSearch {
|
||||
data: Data;
|
||||
total: number;
|
||||
}
|
||||
interface Data {
|
||||
books?: BooksEntity[] | null;
|
||||
}
|
||||
|
||||
+2636
-2643
File diff suppressed because it is too large
Load Diff
+263
-264
@@ -1,264 +1,263 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class Jaomix implements Plugin.PluginBase {
|
||||
id = "jaomix.ru";
|
||||
name = "Jaomix";
|
||||
site = "https://jaomix.ru";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/jaomix/icon.png";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/?searchrn";
|
||||
|
||||
if (filters?.lang?.value?.length) {
|
||||
url += filters.lang.value
|
||||
.map((lang, idx) => `&lang[${idx}]=${lang}`).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}`).join("");
|
||||
}
|
||||
|
||||
url += "&sortcountchapt=" + (filters?.sortcountchapt?.value || "1");
|
||||
url += "&sortdaycreate=" + (filters?.sortdaycreate?.value || "1");
|
||||
url +=
|
||||
"&sortby=" + (showLatestNovels ? "upd" : filters?.sortby?.value || "topweek");
|
||||
url += "&gpage=" + pageNo;
|
||||
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
|
||||
function () {
|
||||
const name = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("title");
|
||||
const cover = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a > img')
|
||||
.attr("src")
|
||||
?.replace("-150x150", "");
|
||||
const url = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("href");
|
||||
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
},
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
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] === "Автор:") {
|
||||
novel.author = text.splice(1).join(" ");
|
||||
} else if (text[0] === "Жанры:") {
|
||||
novel.genres = text.splice(1).join(",");
|
||||
} else if (text[0] === "Статус:") {
|
||||
novel.status = text.includes("продолжается")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed;
|
||||
}
|
||||
});
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".download-chapter div.title").each(function () {
|
||||
const name = loadedCheerio(this).find("a").attr("title");
|
||||
const releaseTime = loadedCheerio(this).find("time").text();
|
||||
const url = loadedCheerio(this).find("a").attr("href");
|
||||
if (!name || !url) return;
|
||||
|
||||
chapters.push({ name, releaseTime, url });
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
loadedCheerio('div[class="adblock-service"]').remove();
|
||||
const chapterText = loadedCheerio('div[class="entry-content"]').html();
|
||||
|
||||
return chapterText || "";
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd&gpage=${pageNo}`;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
|
||||
function () {
|
||||
const name = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("title");
|
||||
const cover = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a > img')
|
||||
.attr("src")
|
||||
?.replace("-150x150", "");
|
||||
const url = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("href");
|
||||
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
},
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sortby: {
|
||||
label: "Сортировка:",
|
||||
value: "topweek",
|
||||
options: [
|
||||
{ label: "Топ недели", value: "topweek" },
|
||||
{ label: "По алфавиту", value: "alphabet" },
|
||||
{ label: "По дате обновления", value: "upd" },
|
||||
{ label: "По дате создания", value: "new" },
|
||||
{ label: "По просмотрам", value: "count" },
|
||||
{ label: "Топ года", value: "topyear" },
|
||||
{ label: "Топ дня", value: "topday" },
|
||||
{ label: "Топ за все время", value: "alltime" },
|
||||
{ label: "Топ месяца", value: "topmonth" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
sortdaycreate: {
|
||||
label: "Дата добавления:",
|
||||
value: "1",
|
||||
options: [
|
||||
{ label: "Любое", value: "1" },
|
||||
{ label: "От 120 до 180 дней", value: "1218" },
|
||||
{ label: "От 180 до 365 дней", value: "1836" },
|
||||
{ label: "От 30 до 60 дней", value: "3060" },
|
||||
{ label: "От 365 дней", value: "365" },
|
||||
{ label: "От 60 до 90 дней", value: "6090" },
|
||||
{ label: "От 90 до 120 дней", value: "9012" },
|
||||
{ label: "Послед. 30 дней", value: "30" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
sortcountchapt: {
|
||||
label: "Количество глав:",
|
||||
value: "1",
|
||||
options: [
|
||||
{ label: "Любое кол-во глав", value: "1" },
|
||||
{ label: "До 500", value: "500" },
|
||||
{ label: "От 1000 до 2000", value: "1020" },
|
||||
{ label: "От 2000 до 3000", value: "2030" },
|
||||
{ label: "От 3000 до 4000", value: "3040" },
|
||||
{ label: "От 4000", value: "400" },
|
||||
{ label: "От 500 до 1000", value: "510" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
label: "Жанры:",
|
||||
value: { include: [], exclude: [] },
|
||||
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: "Adult", value: "Adult" },
|
||||
{ label: "Ecchi", value: "Ecchi" },
|
||||
{ label: "Josei", value: "Josei" },
|
||||
{ label: "Lolicon", value: "Lolicon" },
|
||||
{ label: "Mature", value: "Mature" },
|
||||
{ label: "Shoujo", value: "Shoujo" },
|
||||
{ label: "Wuxia", value: "Wuxia" },
|
||||
{ label: "Xianxia", value: "Xianxia" },
|
||||
{ label: "Xuanhuan", value: "Xuanhuan" },
|
||||
{ label: "Yaoi", value: "Yaoi" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
lang: {
|
||||
label: "Выбрать языки:",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Английский", value: "Английский" },
|
||||
{ label: "Китайский", value: "Китайский" },
|
||||
{ label: "Корейский", value: "Корейский" },
|
||||
{ label: "Японский", value: "Японский" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class Jaomix implements Plugin.PluginBase {
|
||||
id = "jaomix.ru";
|
||||
name = "Jaomix";
|
||||
site = "https://jaomix.ru";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/jaomix/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/?searchrn";
|
||||
|
||||
if (filters?.lang?.value?.length) {
|
||||
url += filters.lang.value
|
||||
.map((lang, idx) => `&lang[${idx}]=${lang}`)
|
||||
.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}`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
url += "&sortcountchapt=" + (filters?.sortcountchapt?.value || "1");
|
||||
url += "&sortdaycreate=" + (filters?.sortdaycreate?.value || "1");
|
||||
url += "&sortby=" + (showLatestNovels ? "upd" : filters?.sortby?.value || "topweek");
|
||||
url += "&gpage=" + pageNo;
|
||||
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
|
||||
function () {
|
||||
const name = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("title");
|
||||
const cover = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a > img')
|
||||
.attr("src")
|
||||
?.replace("-150x150", "");
|
||||
const url = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("href");
|
||||
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
},
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
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] === "Автор:") {
|
||||
novel.author = text.splice(1).join(" ");
|
||||
} else if (text[0] === "Жанры:") {
|
||||
novel.genres = text.splice(1).join(",");
|
||||
} else if (text[0] === "Статус:") {
|
||||
novel.status = text.includes("продолжается")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed;
|
||||
}
|
||||
});
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
const totalChapters = loadedCheerio(".download-chapter div.title").length;
|
||||
|
||||
loadedCheerio(".download-chapter div.title").each((chapterIndex, element) => {
|
||||
const name = loadedCheerio(element).find("a").attr("title");
|
||||
const url = loadedCheerio(element).find("a").attr("href");
|
||||
if (!name || !url) return;
|
||||
|
||||
const releaseTime = loadedCheerio(element).find("time").text();
|
||||
chapters.push({ name, url, releaseTime, 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);
|
||||
|
||||
loadedCheerio('div[class="adblock-service"]').remove();
|
||||
const chapterText = loadedCheerio('div[class="entry-content"]').html();
|
||||
|
||||
return chapterText || "";
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd&gpage=${pageNo}`;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
|
||||
function () {
|
||||
const name = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("title");
|
||||
const cover = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a > img')
|
||||
.attr("src")
|
||||
?.replace("-150x150", "");
|
||||
const url = loadedCheerio(this)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("href");
|
||||
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
},
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sortby: {
|
||||
label: "Сортировка:",
|
||||
value: "topweek",
|
||||
options: [
|
||||
{ label: "Топ недели", value: "topweek" },
|
||||
{ label: "По алфавиту", value: "alphabet" },
|
||||
{ label: "По дате обновления", value: "upd" },
|
||||
{ label: "По дате создания", value: "new" },
|
||||
{ label: "По просмотрам", value: "count" },
|
||||
{ label: "Топ года", value: "topyear" },
|
||||
{ label: "Топ дня", value: "topday" },
|
||||
{ label: "Топ за все время", value: "alltime" },
|
||||
{ label: "Топ месяца", value: "topmonth" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
sortdaycreate: {
|
||||
label: "Дата добавления:",
|
||||
value: "1",
|
||||
options: [
|
||||
{ label: "Любое", value: "1" },
|
||||
{ label: "От 120 до 180 дней", value: "1218" },
|
||||
{ label: "От 180 до 365 дней", value: "1836" },
|
||||
{ label: "От 30 до 60 дней", value: "3060" },
|
||||
{ label: "От 365 дней", value: "365" },
|
||||
{ label: "От 60 до 90 дней", value: "6090" },
|
||||
{ label: "От 90 до 120 дней", value: "9012" },
|
||||
{ label: "Послед. 30 дней", value: "30" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
sortcountchapt: {
|
||||
label: "Количество глав:",
|
||||
value: "1",
|
||||
options: [
|
||||
{ label: "Любое кол-во глав", value: "1" },
|
||||
{ label: "До 500", value: "500" },
|
||||
{ label: "От 1000 до 2000", value: "1020" },
|
||||
{ label: "От 2000 до 3000", value: "2030" },
|
||||
{ label: "От 3000 до 4000", value: "3040" },
|
||||
{ label: "От 4000", value: "400" },
|
||||
{ label: "От 500 до 1000", value: "510" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
label: "Жанры:",
|
||||
value: { include: [], exclude: [] },
|
||||
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: "Adult", value: "Adult" },
|
||||
{ label: "Ecchi", value: "Ecchi" },
|
||||
{ label: "Josei", value: "Josei" },
|
||||
{ label: "Lolicon", value: "Lolicon" },
|
||||
{ label: "Mature", value: "Mature" },
|
||||
{ label: "Shoujo", value: "Shoujo" },
|
||||
{ label: "Wuxia", value: "Wuxia" },
|
||||
{ label: "Xianxia", value: "Xianxia" },
|
||||
{ label: "Xuanhuan", value: "Xuanhuan" },
|
||||
{ label: "Yaoi", value: "Yaoi" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
lang: {
|
||||
label: "Выбрать языки:",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Английский", value: "Английский" },
|
||||
{ label: "Китайский", value: "Китайский" },
|
||||
{ label: "Корейский", value: "Корейский" },
|
||||
{ label: "Японский", value: "Японский" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new Jaomix();
|
||||
|
||||
+963
-957
File diff suppressed because it is too large
Load Diff
+1269
-1284
File diff suppressed because it is too large
Load Diff
+484
-512
@@ -1,512 +1,484 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class RLIB implements Plugin.PluginBase {
|
||||
id = "RLIB";
|
||||
name = "RanobeLib";
|
||||
site = "https://ranobelib.me";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/ranobelib/icon.png";
|
||||
ui: string | number | undefined = undefined;
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = `${this.site}/manga-list?sort=`;
|
||||
url += showLatestNovels
|
||||
? "last_chapter_at"
|
||||
: filters?.sort?.value || "rate";
|
||||
url += "&dir=" + (filters?.order?.value || "desc");
|
||||
|
||||
if (filters.type?.value?.length) {
|
||||
url += filters.type.value.map((i) => "&types[]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.format?.value?.include?.length) {
|
||||
url += filters.format.value.include
|
||||
.map((i) => "&format[include][]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.format?.value?.exclude?.length) {
|
||||
url += filters.format.value.exclude
|
||||
.map((i) => "&format[exclude][]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.status?.value?.length) {
|
||||
url += filters.status.value.map((i) => "&status[]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.statuss?.value?.length) {
|
||||
url += filters.statuss.value.map((i) => "&manga_status[]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.genres?.value?.include?.length) {
|
||||
url += filters.genres.value.include
|
||||
.map((i) => "&genres[include][]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.genres?.value?.exclude?.length) {
|
||||
url += filters.genres.value.exclude
|
||||
.map((i) => "&genres[exclude][]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.tags?.value?.include?.length) {
|
||||
url += filters.tags.value.include
|
||||
.map((i) => "&tags[include][]=" + i).join("");
|
||||
}
|
||||
|
||||
if (filters.tags?.value?.exclude?.length) {
|
||||
url += filters.tags.value.exclude
|
||||
.map((i) => "&tags[exclude][]=" + i).join("");
|
||||
}
|
||||
|
||||
url += "&page=" + pageNo;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
this.ui = loadedCheerio("a.header-right-menu__item")
|
||||
.attr("href")
|
||||
?.replace?.(/[^0-9]/g, "");
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio(".media-card-wrap").each(function () {
|
||||
const name = loadedCheerio(this).find(".media-card__title").text();
|
||||
const cover = loadedCheerio(this).find("a.media-card").attr("data-src");
|
||||
const url = loadedCheerio(this).find("a.media-card").attr("href");
|
||||
if (!url) return;
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".media-name__main").text().trim();
|
||||
|
||||
novel.cover = loadedCheerio(".media-sidebar__cover img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio(".media-description__text").text().trim();
|
||||
|
||||
novel.genres = loadedCheerio('div[class="media-tags"]')
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/[\n\r]+/g, ", ")
|
||||
.replace(/ /g, "");
|
||||
|
||||
loadedCheerio(
|
||||
'div[class="media-info-list paper"] > [class="media-info-list__item"]',
|
||||
).each(function () {
|
||||
let name = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__title"]')
|
||||
.text();
|
||||
|
||||
if (name === "Статус перевода") {
|
||||
novel.status =
|
||||
loadedCheerio(this).find("div:nth-child(2)").text().trim() ===
|
||||
"Продолжается"
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed;
|
||||
} else if (name === "Автор") {
|
||||
novel.author = loadedCheerio(this)
|
||||
.find("div:nth-child(2)")
|
||||
.text()
|
||||
.trim();
|
||||
}
|
||||
});
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
let chaptersRaw: any = body.match(
|
||||
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm,
|
||||
);
|
||||
|
||||
chaptersRaw = chaptersRaw?.[0]
|
||||
?.replace("window.__DATA__ = ", "")
|
||||
?.replace("window._SITE_COLOR_", "")
|
||||
?.trim()
|
||||
?.slice(0, -1);
|
||||
|
||||
let chaptersJson: responseBook = JSON.parse(chaptersRaw);
|
||||
this.ui = chaptersJson?.user?.id;
|
||||
|
||||
chaptersJson.chapters?.list?.forEach((chapter) =>
|
||||
chapters.push({
|
||||
name: `Том ${chapter.chapter_volume} Глава ${chapter.chapter_number} ${chapter.chapter_name}`?.trim(),
|
||||
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
|
||||
url:
|
||||
`${this.site}/${chaptersJson.manga.slug}/v${chapter.chapter_volume}/c${chapter.chapter_number}?bid=` +
|
||||
(chapter?.branch_id || ""),
|
||||
}),
|
||||
);
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(
|
||||
chapterUrl + (this.ui ? "&ui=" + this.ui : ""),
|
||||
);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const baseUrl = this.site;
|
||||
loadedCheerio(".reader-container img").each(function () {
|
||||
const src =
|
||||
loadedCheerio(this).attr("data-src") || loadedCheerio(this).attr("src");
|
||||
if (!src?.startsWith("http")) {
|
||||
loadedCheerio(this).attr("src", baseUrl + src);
|
||||
} else {
|
||||
loadedCheerio(this).attr("src", src);
|
||||
}
|
||||
loadedCheerio(this).removeAttr("data-src");
|
||||
});
|
||||
|
||||
const chapterText = loadedCheerio(".reader-container").html();
|
||||
|
||||
return chapterText || "";
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(
|
||||
`${this.site}/search?q=${searchTerm}&type=manga`,
|
||||
);
|
||||
const body = (await result.json()) as Manga[];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
body.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel?.rus_name || novel.name,
|
||||
cover: novel?.coverImage,
|
||||
url: novel?.href || this.site + "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Рейтинг", value: "rate" },
|
||||
{ label: "Имя", value: "name" },
|
||||
{ label: "Просмотры", value: "views" },
|
||||
{ label: "Дате добавления", value: "created_at" },
|
||||
{ label: "Дате обновления", value: "last_chapter_at" },
|
||||
{ label: "Количество глав", value: "chap_count" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
order: {
|
||||
label: "Порядок",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "По убыванию", value: "desc" },
|
||||
{ label: "По возрастанию", value: "asc" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
type: {
|
||||
label: "Тип",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Авторский", value: "14" },
|
||||
{ label: "Английский", value: "13" },
|
||||
{ label: "Китай", value: "12" },
|
||||
{ label: "Корея", value: "11" },
|
||||
{ label: "Фанфик", value: "15" },
|
||||
{ label: "Япония", value: "10" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
format: {
|
||||
label: "Формат выпуска",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "4-кома (Ёнкома)", value: "1" },
|
||||
{ label: "В цвете", value: "4" },
|
||||
{ label: "Веб", value: "6" },
|
||||
{ label: "Вебтун", value: "7" },
|
||||
{ label: "Додзинси", value: "3" },
|
||||
{ label: "Сборник", value: "2" },
|
||||
{ label: "Сингл", value: "5" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
status: {
|
||||
label: "Статус перевода",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Продолжается", value: "1" },
|
||||
{ label: "Завершен", value: "2" },
|
||||
{ label: "Заморожен", value: "3" },
|
||||
{ label: "Заброшен", value: "4" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
statuss: {
|
||||
label: "Статус тайтла",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Онгоинг", value: "1" },
|
||||
{ label: "Завершён", value: "2" },
|
||||
{ label: "Анонс", value: "3" },
|
||||
{ label: "Приостановлен", value: "4" },
|
||||
{ label: "Выпуск прекращён", value: "5" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
genres: {
|
||||
label: "Жанры",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "Арт", value: "32" },
|
||||
{ label: "Безумие", value: "91" },
|
||||
{ label: "Боевик", value: "34" },
|
||||
{ label: "Боевые искусства", value: "35" },
|
||||
{ label: "Вампиры", value: "36" },
|
||||
{ label: "Военное", value: "89" },
|
||||
{ label: "Гарем", value: "37" },
|
||||
{ label: "Гендерная интрига", value: "38" },
|
||||
{ label: "Героическое фэнтези", value: "39" },
|
||||
{ label: "Демоны", value: "81" },
|
||||
{ label: "Детектив", value: "40" },
|
||||
{ label: "Детское", value: "88" },
|
||||
{ label: "Дзёсэй", value: "41" },
|
||||
{ label: "Драма", value: "43" },
|
||||
{ label: "Игра", value: "44" },
|
||||
{ label: "Исекай", value: "79" },
|
||||
{ label: "История", value: "45" },
|
||||
{ label: "Киберпанк", value: "46" },
|
||||
{ label: "Кодомо", value: "76" },
|
||||
{ label: "Комедия", value: "47" },
|
||||
{ label: "Космос", value: "83" },
|
||||
{ label: "Магия", value: "85" },
|
||||
{ label: "Махо-сёдзё", value: "48" },
|
||||
{ label: "Машины", value: "90" },
|
||||
{ label: "Меха", value: "49" },
|
||||
{ label: "Мистика", value: "50" },
|
||||
{ label: "Музыка", value: "80" },
|
||||
{ label: "Научная фантастика", value: "51" },
|
||||
{ label: "Омегаверс", value: "77" },
|
||||
{ label: "Пародия", value: "86" },
|
||||
{ label: "Повседневность", value: "52" },
|
||||
{ label: "Полиция", value: "82" },
|
||||
{ label: "Постапокалиптика", value: "53" },
|
||||
{ label: "Приключения", value: "54" },
|
||||
{ label: "Психология", value: "55" },
|
||||
{ label: "Романтика", value: "56" },
|
||||
{ label: "Самурайский боевик", value: "57" },
|
||||
{ label: "Сверхъестественное", value: "58" },
|
||||
{ label: "Сёдзё", value: "59" },
|
||||
{ label: "Сёдзё-ай", value: "60" },
|
||||
{ label: "Сёнэн", value: "61" },
|
||||
{ label: "Сёнэн-ай", value: "62" },
|
||||
{ label: "Спорт", value: "63" },
|
||||
{ label: "Супер сила", value: "87" },
|
||||
{ label: "Сэйнэн", value: "64" },
|
||||
{ label: "Трагедия", value: "65" },
|
||||
{ label: "Триллер", value: "66" },
|
||||
{ label: "Ужасы", value: "67" },
|
||||
{ label: "Фантастика", value: "68" },
|
||||
{ label: "Фэнтези", value: "69" },
|
||||
{ label: "Школа", value: "70" },
|
||||
{ label: "Эротика", value: "71" },
|
||||
{ label: "Этти", value: "72" },
|
||||
{ label: "Юри", value: "73" },
|
||||
{ label: "Яой", value: "74" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
tags: {
|
||||
label: "Теги",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "Авантюристы", value: "328" },
|
||||
{ label: "Антигерой", value: "176" },
|
||||
{ label: "Бессмертные", value: "333" },
|
||||
{ label: "Боги", value: "218" },
|
||||
{ label: "Борьба за власть", value: "309" },
|
||||
{ label: "Брат и сестра", value: "360" },
|
||||
{ label: "Ведьма", value: "339" },
|
||||
{ label: "Видеоигры", value: "204" },
|
||||
{ label: "Виртуальная реальность", value: "214" },
|
||||
{ label: "Владыка демонов", value: "349" },
|
||||
{ label: "Военные", value: "198" },
|
||||
{ label: "Воспоминания из другого мира", value: "310" },
|
||||
{ label: "Выживание", value: "212" },
|
||||
{ label: "ГГ женщина", value: "294" },
|
||||
{ label: "ГГ имба", value: "292" },
|
||||
{ label: "ГГ мужчина", value: "295" },
|
||||
{ label: "ГГ не ояш", value: "325" },
|
||||
{ label: "ГГ не человек", value: "331" },
|
||||
{ label: "ГГ ояш", value: "326" },
|
||||
{ label: "Главный герой бог", value: "324" },
|
||||
{ label: "Глупый ГГ", value: "298" },
|
||||
{ label: "Горничные", value: "171" },
|
||||
{ label: "Гуро", value: "306" },
|
||||
{ label: "Гяру", value: "197" },
|
||||
{ label: "Демоны", value: "157" },
|
||||
{ label: "Драконы", value: "313" },
|
||||
{ label: "Древний мир", value: "317" },
|
||||
{ label: "Зверолюди", value: "163" },
|
||||
{ label: "Зомби", value: "155" },
|
||||
{ label: "Исторические фигуры", value: "323" },
|
||||
{ label: "Кулинария", value: "158" },
|
||||
{ label: "Культивация", value: "161" },
|
||||
{ label: "ЛГБТ", value: "344" },
|
||||
{ label: "ЛитРПГ", value: "319" },
|
||||
{ label: "Лоли", value: "206" },
|
||||
{ label: "Магия", value: "170" },
|
||||
{ label: "Машинный перевод", value: "345" },
|
||||
{ label: "Медицина", value: "159" },
|
||||
{ label: "Межгалактическая война", value: "330" },
|
||||
{ label: "Монстр Девушки", value: "207" },
|
||||
{ label: "Монстры", value: "208" },
|
||||
{ label: "Мрачный мир", value: "316" },
|
||||
{ label: "Музыка", value: "358" },
|
||||
{ label: "Музыка", value: "209" },
|
||||
{ label: "Ниндзя", value: "199" },
|
||||
{ label: "Обратный Гарем", value: "210" },
|
||||
{ label: "Офисные Работники", value: "200" },
|
||||
{ label: "Пираты", value: "341" },
|
||||
{ label: "Подземелья", value: "314" },
|
||||
{ label: "Политика", value: "311" },
|
||||
{ label: "Полиция", value: "201" },
|
||||
{ label: "Преступники / Криминал", value: "205" },
|
||||
{ label: "Призраки / Духи", value: "196" },
|
||||
{ label: "Призыватели", value: "329" },
|
||||
{ label: "Прыжки между мирами", value: "321" },
|
||||
{ label: "Путешествие в другой мир", value: "318" },
|
||||
{ label: "Путешествие во времени", value: "213" },
|
||||
{ label: "Рабы", value: "355" },
|
||||
{ label: "Ранги силы", value: "312" },
|
||||
{ label: "Реинкарнация", value: "154" },
|
||||
{ label: "Самураи", value: "202" },
|
||||
{ label: "Скрытие личности", value: "315" },
|
||||
{ label: "Средневековье", value: "174" },
|
||||
{ label: "Традиционные игры", value: "203" },
|
||||
{ label: "Умный ГГ", value: "303" },
|
||||
{ label: "Характерный рост", value: "332" },
|
||||
{ label: "Хикикомори", value: "167" },
|
||||
{ label: "Эволюция", value: "322" },
|
||||
{ label: "Элементы РПГ", value: "327" },
|
||||
{ label: "Эльфы", value: "217" },
|
||||
{ label: "Якудза", value: "165" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new RLIB();
|
||||
|
||||
interface responseBook {
|
||||
hasStickyPermission: boolean;
|
||||
bookmark?: null;
|
||||
auth: boolean;
|
||||
comments_version: string;
|
||||
manga: Manga;
|
||||
chapters: Chapters;
|
||||
user?: User;
|
||||
}
|
||||
interface Manga {
|
||||
id: number;
|
||||
name: string;
|
||||
rusName?: string;
|
||||
rus_name?: string;
|
||||
engName?: string;
|
||||
slug: string;
|
||||
status: number;
|
||||
chapters_count: number;
|
||||
altNames?: string[] | null;
|
||||
coverImage?: string;
|
||||
href?: string;
|
||||
}
|
||||
interface Chapters {
|
||||
list?: ListEntity[] | null;
|
||||
teams?: TeamsEntity[] | null;
|
||||
branches?: BranchesEntity[] | null;
|
||||
is_paid?: null[] | null;
|
||||
}
|
||||
interface ListEntity {
|
||||
chapter_id: number;
|
||||
chapter_slug: string;
|
||||
chapter_name: string;
|
||||
chapter_number: string;
|
||||
chapter_volume: number;
|
||||
chapter_moderated: number;
|
||||
chapter_user_id: number;
|
||||
chapter_expired_at: string;
|
||||
chapter_scanlator_id: number;
|
||||
chapter_created_at: string;
|
||||
status?: null;
|
||||
price: number;
|
||||
branch_id: number;
|
||||
username: string;
|
||||
}
|
||||
interface TeamsEntity {
|
||||
name: string;
|
||||
alt_name: string;
|
||||
cover: string;
|
||||
slug: string;
|
||||
id: number;
|
||||
branch_id: number;
|
||||
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;
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const statusKey = [
|
||||
NovelStatus.Ongoing,
|
||||
NovelStatus.Completed,
|
||||
NovelStatus.OnHiatus,
|
||||
NovelStatus.Cancelled,
|
||||
];
|
||||
|
||||
class RLIB implements Plugin.PluginBase {
|
||||
id = "RLIB";
|
||||
name = "RanobeLib";
|
||||
site = "https://ranobelib.me";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/ranobelib/icon.png";
|
||||
ui: string | undefined = undefined;
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/manga-list?sort=";
|
||||
url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate";
|
||||
url += "&dir=" + (filters?.order?.value || "desc");
|
||||
|
||||
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
|
||||
if (value instanceof Array && value.length) {
|
||||
url += "&" + type + "[]=" + value.join("&" + type + "[]=");
|
||||
}
|
||||
if (value?.include instanceof Array && value.include.length) {
|
||||
url += "&" + type + "[include][]=" +
|
||||
value.include.join("&" + type + "[include][]=");
|
||||
}
|
||||
if (value?.exclude instanceof Array && value.exclude.length) {
|
||||
url += "&" + type + "[exclude][]=" +
|
||||
value.exclude.join("&" + type + "[exclude][]=");
|
||||
}
|
||||
});
|
||||
|
||||
url += "&page=" + pageNo;
|
||||
|
||||
const result = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
this.ui = loadedCheerio("a.header-right-menu__item")
|
||||
.attr("href")
|
||||
?.replace?.(/[^0-9]/g, "");
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio(".media-card-wrap").each(function () {
|
||||
const name = loadedCheerio(this).find(".media-card__title").text();
|
||||
const cover = loadedCheerio(this).find("a.media-card").attr("data-src");
|
||||
const url = loadedCheerio(this).find("a.media-card").attr("href");
|
||||
if (!url) return;
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".media-name__main").text().trim();
|
||||
|
||||
novel.cover = loadedCheerio(".container_responsive img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio(".media-description__text").text().trim();
|
||||
|
||||
novel.genres = loadedCheerio('div[class="media-tags"]')
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/[\n\r]+/g, ", ")
|
||||
.replace(/ /g, "");
|
||||
|
||||
loadedCheerio(
|
||||
'div[class="media-info-list paper"] > [class="media-info-list__item"]',
|
||||
).each(function () {
|
||||
let name = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__title"]')
|
||||
.text();
|
||||
|
||||
if (name === "Автор") {
|
||||
novel.author = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__value"]')
|
||||
.text()
|
||||
.trim();
|
||||
} else if (name === "Художник") {
|
||||
novel.artist = loadedCheerio(this)
|
||||
.find('div[class="media-info-list__value"]')
|
||||
.text()
|
||||
.trim();
|
||||
}
|
||||
});
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
let chaptersRaw: any = body.match(
|
||||
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm,
|
||||
);
|
||||
|
||||
chaptersRaw = chaptersRaw?.[0]
|
||||
?.replace("window.__DATA__ = ", "")
|
||||
?.replace("window._SITE_COLOR_", "")
|
||||
?.trim()
|
||||
?.slice(0, -1);
|
||||
|
||||
const chaptersJson: responseBook = JSON.parse(chaptersRaw);
|
||||
const totalChapters = chaptersJson.chapters.list?.length || 0;
|
||||
novel.status = statusKey[chaptersJson.manga.status - 1] || NovelStatus.Unknown;
|
||||
this.ui = chaptersJson?.user?.id;
|
||||
|
||||
chaptersJson.chapters?.list?.forEach((chapter, chapterIndex) =>
|
||||
chapters.push({
|
||||
name:
|
||||
"Том " + chapter.chapter_volume +
|
||||
" Глава " + chapter.chapter_number +
|
||||
(chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
|
||||
url:
|
||||
this.site + "/" + chaptersJson.manga.slug +
|
||||
"/v" + chapter.chapter_volume + "/c" + chapter.chapter_number +
|
||||
"?bid=" + (chapter.branch_id || ""),
|
||||
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
|
||||
chapterNumber: totalChapters - chapterIndex,
|
||||
}),
|
||||
);
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(
|
||||
chapterUrl + (this.ui ? "&ui=" + this.ui : ""),
|
||||
).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(result);
|
||||
loadedCheerio(".reader-container img").each((index, element) => {
|
||||
const src =
|
||||
loadedCheerio(element).attr("data-src") ||
|
||||
loadedCheerio(element).attr("src");
|
||||
if (!src?.startsWith("http")) {
|
||||
loadedCheerio(element).attr("src", this.site + src);
|
||||
} else {
|
||||
loadedCheerio(element).attr("src", src);
|
||||
}
|
||||
loadedCheerio(element).removeAttr("data-src");
|
||||
});
|
||||
|
||||
const chapterText = loadedCheerio(".reader-container").html();
|
||||
return chapterText || "";
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(
|
||||
`${this.site}/search?q=${searchTerm}&type=manga`,
|
||||
);
|
||||
const body = (await result.json()) as Manga[];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
body.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel?.rus_name || novel.name,
|
||||
cover: novel?.coverImage,
|
||||
url: novel?.href || this.site + "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка",
|
||||
value: "rate",
|
||||
options: [
|
||||
{ label: "Рейтинг", value: "rate" },
|
||||
{ label: "Имя", value: "name" },
|
||||
{ label: "Просмотры", value: "views" },
|
||||
{ label: "Дате добавления", value: "created_at" },
|
||||
{ label: "Дате обновления", value: "last_chapter_at" },
|
||||
{ label: "Количество глав", value: "chap_count" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
order: {
|
||||
label: "Порядок",
|
||||
value: "desc",
|
||||
options: [
|
||||
{ label: "По убыванию", value: "desc" },
|
||||
{ label: "По возрастанию", value: "asc" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
type: {
|
||||
label: "Тип",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Авторский", value: "14" },
|
||||
{ label: "Английский", value: "13" },
|
||||
{ label: "Китай", value: "12" },
|
||||
{ label: "Корея", value: "11" },
|
||||
{ label: "Фанфик", value: "15" },
|
||||
{ label: "Япония", value: "10" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
format: {
|
||||
label: "Формат выпуска",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "4-кома (Ёнкома)", value: "1" },
|
||||
{ label: "В цвете", value: "4" },
|
||||
{ label: "Веб", value: "6" },
|
||||
{ label: "Вебтун", value: "7" },
|
||||
{ label: "Додзинси", value: "3" },
|
||||
{ label: "Сборник", value: "2" },
|
||||
{ label: "Сингл", value: "5" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
status: {
|
||||
label: "Статус перевода",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Продолжается", value: "1" },
|
||||
{ label: "Завершен", value: "2" },
|
||||
{ label: "Заморожен", value: "3" },
|
||||
{ label: "Заброшен", value: "4" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
manga_status: {
|
||||
label: "Статус тайтла",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Онгоинг", value: "1" },
|
||||
{ label: "Завершён", value: "2" },
|
||||
{ label: "Анонс", value: "3" },
|
||||
{ label: "Приостановлен", value: "4" },
|
||||
{ label: "Выпуск прекращён", value: "5" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
genres: {
|
||||
label: "Жанры",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "Арт", value: "32" },
|
||||
{ label: "Безумие", value: "91" },
|
||||
{ label: "Боевик", value: "34" },
|
||||
{ label: "Боевые искусства", value: "35" },
|
||||
{ label: "Вампиры", value: "36" },
|
||||
{ label: "Военное", value: "89" },
|
||||
{ label: "Гарем", value: "37" },
|
||||
{ label: "Гендерная интрига", value: "38" },
|
||||
{ label: "Героическое фэнтези", value: "39" },
|
||||
{ label: "Демоны", value: "81" },
|
||||
{ label: "Детектив", value: "40" },
|
||||
{ label: "Детское", value: "88" },
|
||||
{ label: "Дзёсэй", value: "41" },
|
||||
{ label: "Драма", value: "43" },
|
||||
{ label: "Игра", value: "44" },
|
||||
{ label: "Исекай", value: "79" },
|
||||
{ label: "История", value: "45" },
|
||||
{ label: "Киберпанк", value: "46" },
|
||||
{ label: "Кодомо", value: "76" },
|
||||
{ label: "Комедия", value: "47" },
|
||||
{ label: "Космос", value: "83" },
|
||||
{ label: "Магия", value: "85" },
|
||||
{ label: "Махо-сёдзё", value: "48" },
|
||||
{ label: "Машины", value: "90" },
|
||||
{ label: "Меха", value: "49" },
|
||||
{ label: "Мистика", value: "50" },
|
||||
{ label: "Музыка", value: "80" },
|
||||
{ label: "Научная фантастика", value: "51" },
|
||||
{ label: "Омегаверс", value: "77" },
|
||||
{ label: "Пародия", value: "86" },
|
||||
{ label: "Повседневность", value: "52" },
|
||||
{ label: "Полиция", value: "82" },
|
||||
{ label: "Постапокалиптика", value: "53" },
|
||||
{ label: "Приключения", value: "54" },
|
||||
{ label: "Психология", value: "55" },
|
||||
{ label: "Романтика", value: "56" },
|
||||
{ label: "Самурайский боевик", value: "57" },
|
||||
{ label: "Сверхъестественное", value: "58" },
|
||||
{ label: "Сёдзё", value: "59" },
|
||||
{ label: "Сёдзё-ай", value: "60" },
|
||||
{ label: "Сёнэн", value: "61" },
|
||||
{ label: "Сёнэн-ай", value: "62" },
|
||||
{ label: "Спорт", value: "63" },
|
||||
{ label: "Супер сила", value: "87" },
|
||||
{ label: "Сэйнэн", value: "64" },
|
||||
{ label: "Трагедия", value: "65" },
|
||||
{ label: "Триллер", value: "66" },
|
||||
{ label: "Ужасы", value: "67" },
|
||||
{ label: "Фантастика", value: "68" },
|
||||
{ label: "Фэнтези", value: "69" },
|
||||
{ label: "Школа", value: "70" },
|
||||
{ label: "Эротика", value: "71" },
|
||||
{ label: "Этти", value: "72" },
|
||||
{ label: "Юри", value: "73" },
|
||||
{ label: "Яой", value: "74" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
tags: {
|
||||
label: "Теги",
|
||||
value: { include: [], exclude: [] },
|
||||
options: [
|
||||
{ label: "Авантюристы", value: "328" },
|
||||
{ label: "Антигерой", value: "176" },
|
||||
{ label: "Бессмертные", value: "333" },
|
||||
{ label: "Боги", value: "218" },
|
||||
{ label: "Борьба за власть", value: "309" },
|
||||
{ label: "Брат и сестра", value: "360" },
|
||||
{ label: "Ведьма", value: "339" },
|
||||
{ label: "Видеоигры", value: "204" },
|
||||
{ label: "Виртуальная реальность", value: "214" },
|
||||
{ label: "Владыка демонов", value: "349" },
|
||||
{ label: "Военные", value: "198" },
|
||||
{ label: "Воспоминания из другого мира", value: "310" },
|
||||
{ label: "Выживание", value: "212" },
|
||||
{ label: "ГГ женщина", value: "294" },
|
||||
{ label: "ГГ имба", value: "292" },
|
||||
{ label: "ГГ мужчина", value: "295" },
|
||||
{ label: "ГГ не ояш", value: "325" },
|
||||
{ label: "ГГ не человек", value: "331" },
|
||||
{ label: "ГГ ояш", value: "326" },
|
||||
{ label: "Главный герой бог", value: "324" },
|
||||
{ label: "Глупый ГГ", value: "298" },
|
||||
{ label: "Горничные", value: "171" },
|
||||
{ label: "Гуро", value: "306" },
|
||||
{ label: "Гяру", value: "197" },
|
||||
{ label: "Демоны", value: "157" },
|
||||
{ label: "Драконы", value: "313" },
|
||||
{ label: "Древний мир", value: "317" },
|
||||
{ label: "Зверолюди", value: "163" },
|
||||
{ label: "Зомби", value: "155" },
|
||||
{ label: "Исторические фигуры", value: "323" },
|
||||
{ label: "Кулинария", value: "158" },
|
||||
{ label: "Культивация", value: "161" },
|
||||
{ label: "ЛГБТ", value: "344" },
|
||||
{ label: "ЛитРПГ", value: "319" },
|
||||
{ label: "Лоли", value: "206" },
|
||||
{ label: "Магия", value: "170" },
|
||||
{ label: "Машинный перевод", value: "345" },
|
||||
{ label: "Медицина", value: "159" },
|
||||
{ label: "Межгалактическая война", value: "330" },
|
||||
{ label: "Монстр Девушки", value: "207" },
|
||||
{ label: "Монстры", value: "208" },
|
||||
{ label: "Мрачный мир", value: "316" },
|
||||
{ label: "Музыка", value: "358" },
|
||||
{ label: "Музыка", value: "209" },
|
||||
{ label: "Ниндзя", value: "199" },
|
||||
{ label: "Обратный Гарем", value: "210" },
|
||||
{ label: "Офисные Работники", value: "200" },
|
||||
{ label: "Пираты", value: "341" },
|
||||
{ label: "Подземелья", value: "314" },
|
||||
{ label: "Политика", value: "311" },
|
||||
{ label: "Полиция", value: "201" },
|
||||
{ label: "Преступники / Криминал", value: "205" },
|
||||
{ label: "Призраки / Духи", value: "196" },
|
||||
{ label: "Призыватели", value: "329" },
|
||||
{ label: "Прыжки между мирами", value: "321" },
|
||||
{ label: "Путешествие в другой мир", value: "318" },
|
||||
{ label: "Путешествие во времени", value: "213" },
|
||||
{ label: "Рабы", value: "355" },
|
||||
{ label: "Ранги силы", value: "312" },
|
||||
{ label: "Реинкарнация", value: "154" },
|
||||
{ label: "Самураи", value: "202" },
|
||||
{ label: "Скрытие личности", value: "315" },
|
||||
{ label: "Средневековье", value: "174" },
|
||||
{ label: "Традиционные игры", value: "203" },
|
||||
{ label: "Умный ГГ", value: "303" },
|
||||
{ label: "Характерный рост", value: "332" },
|
||||
{ label: "Хикикомори", value: "167" },
|
||||
{ label: "Эволюция", value: "322" },
|
||||
{ label: "Элементы РПГ", value: "327" },
|
||||
{ label: "Эльфы", value: "217" },
|
||||
{ label: "Якудза", value: "165" },
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new RLIB();
|
||||
|
||||
interface responseBook {
|
||||
hasStickyPermission: boolean;
|
||||
bookmark?: null;
|
||||
auth: boolean;
|
||||
comments_version: string;
|
||||
manga: Manga;
|
||||
chapters: Chapters;
|
||||
user?: User;
|
||||
}
|
||||
interface Manga {
|
||||
id: number;
|
||||
name: string;
|
||||
rusName?: string;
|
||||
rus_name?: string;
|
||||
engName?: string;
|
||||
slug: string;
|
||||
status: number;
|
||||
chapters_count: number;
|
||||
altNames?: string[] | null;
|
||||
coverImage?: string;
|
||||
href?: string;
|
||||
}
|
||||
interface Chapters {
|
||||
list?: ListEntity[] | null;
|
||||
teams?: TeamsEntity[] | null;
|
||||
branches?: BranchesEntity[] | null;
|
||||
is_paid?: null[] | null;
|
||||
}
|
||||
interface ListEntity {
|
||||
chapter_id: number;
|
||||
chapter_slug: string;
|
||||
chapter_name: string;
|
||||
chapter_number: string;
|
||||
chapter_volume: number;
|
||||
chapter_moderated: number;
|
||||
chapter_user_id: number;
|
||||
chapter_expired_at: string;
|
||||
chapter_scanlator_id: number;
|
||||
chapter_created_at: string;
|
||||
status?: null;
|
||||
price: number;
|
||||
branch_id?: number;
|
||||
username: string;
|
||||
}
|
||||
interface TeamsEntity {
|
||||
name: string;
|
||||
alt_name: string;
|
||||
cover: string;
|
||||
slug: string;
|
||||
id: number;
|
||||
branch_id: number;
|
||||
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: string;
|
||||
avatar: string;
|
||||
access: boolean;
|
||||
isAdmin: boolean;
|
||||
paid: boolean;
|
||||
}
|
||||
|
||||
+406
-408
@@ -1,408 +1,406 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class RNRF implements Plugin.PluginBase {
|
||||
id = "RNRF";
|
||||
name = "РанобэРФ";
|
||||
site = "https://ранобэ.рф";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/ranoberf/icon.png";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/books?order=";
|
||||
url += showLatestNovels
|
||||
? "lastPublishedChapter"
|
||||
: filters?.sort?.value || "popular";
|
||||
url += "&page=" + pageNo;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const json: response = JSON.parse(jsonRaw || "{}");
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
json.props.pageProps?.totalData?.items?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel?.verticalImage?.url
|
||||
? this.site + novel.verticalImage.url
|
||||
: defaultCover,
|
||||
url: this.site + "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.title,
|
||||
cover: book?.verticalImage?.url
|
||||
? this.site + book.verticalImage.url
|
||||
: defaultCover,
|
||||
summary: book?.description,
|
||||
author: book?.author || "",
|
||||
genres: book?.genres.map((item) => item.title).join(", "),
|
||||
status: book?.additionalInfo.includes("Активен")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
};
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
book?.chapters?.forEach((chapter) => {
|
||||
if (!chapter.isDonate || chapter.isUserPaid) {
|
||||
chapters.push({
|
||||
name: chapter.title,
|
||||
releaseTime: dayjs(chapter.publishedAt).format("LLL"),
|
||||
url: this.site + chapter.url,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const json: response = JSON.parse(jsonRaw || "{}");
|
||||
|
||||
loadedCheerio = parseHTML(
|
||||
json.props.pageProps?.chapter?.content?.text || "",
|
||||
);
|
||||
|
||||
const baseUrl = this.site;
|
||||
loadedCheerio("img").each(function () {
|
||||
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;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`;
|
||||
const result = await fetchApi(url);
|
||||
const body = (await result.json()) as { items: Item[] };
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
body.items.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel?.verticalImage?.url
|
||||
? this.site + novel.verticalImage.url
|
||||
: defaultCover,
|
||||
url: this.site + "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Рейтинг", value: "popular" },
|
||||
{ label: "Дате добавления", value: "new" },
|
||||
{ label: "Дате обновления", value: "lastPublishedChapter" },
|
||||
{ label: "Законченные", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
export default new RNRF();
|
||||
|
||||
interface response {
|
||||
props: Props;
|
||||
page: string;
|
||||
query: Query;
|
||||
buildId: string;
|
||||
isFallback: boolean;
|
||||
gssp: boolean;
|
||||
appGip: boolean;
|
||||
dynamicIds?: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pageProps: PageProps;
|
||||
isMobile: boolean;
|
||||
theme: string;
|
||||
__N_SSP: boolean;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
initialReduxState: InitialReduxState;
|
||||
book?: PagePropsBook;
|
||||
totalData?: TotalData;
|
||||
layoutViewCookies?: string;
|
||||
displayBanner?: boolean;
|
||||
chapter?: PagePropsChapter;
|
||||
}
|
||||
|
||||
interface PagePropsBook {
|
||||
id: number;
|
||||
countryId: number;
|
||||
creatorId: number;
|
||||
lastChapterId: number;
|
||||
sourceId: number;
|
||||
imgVerticalId: number;
|
||||
imgHorizontalId: number;
|
||||
imgSquareId: number;
|
||||
imageVerticalId: number;
|
||||
imageHorizontalId: number;
|
||||
title: string;
|
||||
titleEn: string;
|
||||
fullTitle: string;
|
||||
fullTitleEn: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
author: string;
|
||||
additionalInfo: string;
|
||||
importantInfo: string;
|
||||
status: string;
|
||||
views: number;
|
||||
likes: number;
|
||||
dislikes: number;
|
||||
intervalSec: number;
|
||||
intervalForNextFreeChapterSec: number;
|
||||
chapterCost: number;
|
||||
chapterPrice: number;
|
||||
chapterPriceCollected: number;
|
||||
maxSubscriptionChapters: number;
|
||||
maxDonateChapters: number;
|
||||
isDisplayOnMainPage: boolean;
|
||||
withoutSchedule: boolean;
|
||||
nextFreeChapterPublishedAt: null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
userRating: null;
|
||||
userBookmark: null;
|
||||
country: Country;
|
||||
genres: Genre[];
|
||||
timer: Timer;
|
||||
chapters: ChapterElement[];
|
||||
verticalImage: Image;
|
||||
horizontalImage: Image;
|
||||
squareImage: Image;
|
||||
}
|
||||
|
||||
interface ChapterElement {
|
||||
id: number;
|
||||
bookId: number;
|
||||
title: string;
|
||||
url: string;
|
||||
numberChapter: string;
|
||||
tom: number | null;
|
||||
chapterShortNumber: number | null;
|
||||
isDonate: boolean;
|
||||
isSponsored: boolean;
|
||||
isSubscription: boolean;
|
||||
isEdited: boolean;
|
||||
publishedAt: Date;
|
||||
price: number;
|
||||
views: number;
|
||||
isUserPaid?: boolean;
|
||||
}
|
||||
|
||||
interface Country {
|
||||
id: number;
|
||||
title: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface Image {
|
||||
id: number;
|
||||
alt: string;
|
||||
path: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Timer {
|
||||
value: null;
|
||||
for: null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface PagePropsChapter {
|
||||
id: number;
|
||||
bookId: number;
|
||||
editorId: null;
|
||||
translatorId: number;
|
||||
parserChapterId: null;
|
||||
title: string;
|
||||
slug: string;
|
||||
status: string;
|
||||
tom: null;
|
||||
views: number;
|
||||
isSponsored: boolean;
|
||||
isSubscription: boolean;
|
||||
isDonate: boolean;
|
||||
isIndexing: boolean;
|
||||
publishedAt: Date;
|
||||
changeAvailabilityAt: null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
numberChapter: string;
|
||||
price: number;
|
||||
content: Content;
|
||||
book: ChapterBook;
|
||||
userBookmark: null;
|
||||
isUserPaid: boolean;
|
||||
nextChapter: null;
|
||||
previousChapter: PreviousChapter;
|
||||
corrections: null;
|
||||
}
|
||||
|
||||
interface ChapterBook {
|
||||
id: number;
|
||||
title: string;
|
||||
titleEn: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Content {
|
||||
id: number;
|
||||
text: string;
|
||||
symbolsCount: number;
|
||||
}
|
||||
|
||||
interface PreviousChapter {
|
||||
id: number;
|
||||
title: string;
|
||||
views: number;
|
||||
publishedAt: Date;
|
||||
url: string;
|
||||
numberChapter: string;
|
||||
}
|
||||
|
||||
interface InitialReduxState {
|
||||
data?: Data;
|
||||
auth: Auth;
|
||||
banners?: Banners;
|
||||
corrections?: Corrections;
|
||||
readerSettings?: ReaderSettings;
|
||||
}
|
||||
|
||||
interface Auth {
|
||||
isAuth: boolean;
|
||||
user: null;
|
||||
}
|
||||
|
||||
interface Banners {
|
||||
displaySale: boolean;
|
||||
}
|
||||
|
||||
interface Corrections {
|
||||
items: null;
|
||||
data: null;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
bookPage: BookPage | null;
|
||||
chapterPage: ChapterPage | null;
|
||||
}
|
||||
|
||||
interface BookPage {
|
||||
bookId: number;
|
||||
bookmark: null;
|
||||
}
|
||||
|
||||
interface ChapterPage {
|
||||
chapterId: number;
|
||||
bookId: number;
|
||||
bookmark: null;
|
||||
}
|
||||
|
||||
interface ReaderSettings {
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
textIndent: number;
|
||||
sidebarHide: number;
|
||||
textAlign: string;
|
||||
}
|
||||
|
||||
interface TotalData {
|
||||
items: Item[];
|
||||
_links: Links;
|
||||
pagesData: PagesData;
|
||||
}
|
||||
|
||||
interface Links {
|
||||
self: First;
|
||||
first: First;
|
||||
last: First;
|
||||
next: First;
|
||||
}
|
||||
|
||||
interface First {
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
status: string;
|
||||
dislikes: number;
|
||||
likes: number;
|
||||
userRating: null;
|
||||
lastChapter: ChapterElement;
|
||||
verticalImage: Image;
|
||||
horizontalImage: Image;
|
||||
squareImage: Image;
|
||||
}
|
||||
|
||||
interface PagesData {
|
||||
totalCount: number;
|
||||
pageCount: number;
|
||||
currentPage: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
interface Query {
|
||||
book?: string;
|
||||
chapter?: string;
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class RNRF implements Plugin.PluginBase {
|
||||
id = "RNRF";
|
||||
name = "РанобэРФ";
|
||||
site = "https://ранобэ.рф";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/ranoberf/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/books?order=";
|
||||
url += showLatestNovels
|
||||
? "lastPublishedChapter"
|
||||
: filters?.sort?.value || "popular";
|
||||
url += "&page=" + pageNo;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const json: response = JSON.parse(jsonRaw || "{}");
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
json.props.pageProps?.totalData?.items?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel?.verticalImage?.url
|
||||
? this.site + novel.verticalImage.url
|
||||
: defaultCover,
|
||||
url: this.site + "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.title,
|
||||
cover: book?.verticalImage?.url
|
||||
? this.site + book.verticalImage.url
|
||||
: defaultCover,
|
||||
summary: book?.description,
|
||||
author: book?.author || "",
|
||||
genres: book?.genres.map((item) => item.title).join(", "),
|
||||
status: book?.additionalInfo.includes("Активен")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
};
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
book?.chapters.forEach((chapter, chapterIndex) => {
|
||||
if (!chapter.isDonate || chapter.isUserPaid) {
|
||||
chapters.push({
|
||||
name: chapter.title,
|
||||
url: this.site + chapter.url,
|
||||
releaseTime: dayjs(chapter.publishedAt).format("LLL"),
|
||||
chapterNumber: book.chapters.length - chapterIndex,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
|
||||
let loadedCheerio = parseHTML(result);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
const json: response = JSON.parse(jsonRaw || "{}");
|
||||
|
||||
loadedCheerio = parseHTML(
|
||||
json.props.pageProps?.chapter?.content?.text || "",
|
||||
);
|
||||
|
||||
loadedCheerio("img").each((index, element) => {
|
||||
if (!loadedCheerio(element).attr("src")?.startsWith("http")) {
|
||||
const src = loadedCheerio(element).attr("src");
|
||||
loadedCheerio(element).attr("src", this.site + src);
|
||||
}
|
||||
});
|
||||
|
||||
const chapterText = loadedCheerio.html();
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`;
|
||||
const result = await fetchApi(url);
|
||||
const body = (await result.json()) as { items: Item[] };
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
body.items.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel?.verticalImage?.url
|
||||
? this.site + novel.verticalImage.url
|
||||
: defaultCover,
|
||||
url: this.site + "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
label: "Сортировка",
|
||||
value: "popular",
|
||||
options: [
|
||||
{ label: "Рейтинг", value: "popular" },
|
||||
{ label: "Дате добавления", value: "new" },
|
||||
{ label: "Дате обновления", value: "lastPublishedChapter" },
|
||||
{ label: "Законченные", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
export default new RNRF();
|
||||
|
||||
interface response {
|
||||
props: Props;
|
||||
page: string;
|
||||
query: Query;
|
||||
buildId: string;
|
||||
isFallback: boolean;
|
||||
gssp: boolean;
|
||||
appGip: boolean;
|
||||
dynamicIds?: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pageProps: PageProps;
|
||||
isMobile: boolean;
|
||||
theme: string;
|
||||
__N_SSP: boolean;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
initialReduxState: InitialReduxState;
|
||||
book?: PagePropsBook;
|
||||
totalData?: TotalData;
|
||||
layoutViewCookies?: string;
|
||||
displayBanner?: boolean;
|
||||
chapter?: PagePropsChapter;
|
||||
}
|
||||
|
||||
interface PagePropsBook {
|
||||
id: number;
|
||||
countryId: number;
|
||||
creatorId: number;
|
||||
lastChapterId: number;
|
||||
sourceId: number;
|
||||
imgVerticalId: number;
|
||||
imgHorizontalId: number;
|
||||
imgSquareId: number;
|
||||
imageVerticalId: number;
|
||||
imageHorizontalId: number;
|
||||
title: string;
|
||||
titleEn: string;
|
||||
fullTitle: string;
|
||||
fullTitleEn: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
author: string;
|
||||
additionalInfo: string;
|
||||
importantInfo: string;
|
||||
status: string;
|
||||
views: number;
|
||||
likes: number;
|
||||
dislikes: number;
|
||||
intervalSec: number;
|
||||
intervalForNextFreeChapterSec: number;
|
||||
chapterCost: number;
|
||||
chapterPrice: number;
|
||||
chapterPriceCollected: number;
|
||||
maxSubscriptionChapters: number;
|
||||
maxDonateChapters: number;
|
||||
isDisplayOnMainPage: boolean;
|
||||
withoutSchedule: boolean;
|
||||
nextFreeChapterPublishedAt: null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
userRating: null;
|
||||
userBookmark: null;
|
||||
country: Country;
|
||||
genres: Genre[];
|
||||
timer: Timer;
|
||||
chapters: ChapterElement[];
|
||||
verticalImage: Image;
|
||||
horizontalImage: Image;
|
||||
squareImage: Image;
|
||||
}
|
||||
|
||||
interface ChapterElement {
|
||||
id: number;
|
||||
bookId: number;
|
||||
title: string;
|
||||
url: string;
|
||||
numberChapter: string;
|
||||
tom: number | null;
|
||||
chapterShortNumber: number | null;
|
||||
isDonate: boolean;
|
||||
isSponsored: boolean;
|
||||
isSubscription: boolean;
|
||||
isEdited: boolean;
|
||||
publishedAt: Date;
|
||||
price: number;
|
||||
views: number;
|
||||
isUserPaid?: boolean;
|
||||
}
|
||||
|
||||
interface Country {
|
||||
id: number;
|
||||
title: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface Image {
|
||||
id: number;
|
||||
alt: string;
|
||||
path: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Timer {
|
||||
value: null;
|
||||
for: null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface PagePropsChapter {
|
||||
id: number;
|
||||
bookId: number;
|
||||
editorId: null;
|
||||
translatorId: number;
|
||||
parserChapterId: null;
|
||||
title: string;
|
||||
slug: string;
|
||||
status: string;
|
||||
tom: null;
|
||||
views: number;
|
||||
isSponsored: boolean;
|
||||
isSubscription: boolean;
|
||||
isDonate: boolean;
|
||||
isIndexing: boolean;
|
||||
publishedAt: Date;
|
||||
changeAvailabilityAt: null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
numberChapter: string;
|
||||
price: number;
|
||||
content: Content;
|
||||
book: ChapterBook;
|
||||
userBookmark: null;
|
||||
isUserPaid: boolean;
|
||||
nextChapter: null;
|
||||
previousChapter: PreviousChapter;
|
||||
corrections: null;
|
||||
}
|
||||
|
||||
interface ChapterBook {
|
||||
id: number;
|
||||
title: string;
|
||||
titleEn: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Content {
|
||||
id: number;
|
||||
text: string;
|
||||
symbolsCount: number;
|
||||
}
|
||||
|
||||
interface PreviousChapter {
|
||||
id: number;
|
||||
title: string;
|
||||
views: number;
|
||||
publishedAt: Date;
|
||||
url: string;
|
||||
numberChapter: string;
|
||||
}
|
||||
|
||||
interface InitialReduxState {
|
||||
data?: Data;
|
||||
auth: Auth;
|
||||
banners?: Banners;
|
||||
corrections?: Corrections;
|
||||
readerSettings?: ReaderSettings;
|
||||
}
|
||||
|
||||
interface Auth {
|
||||
isAuth: boolean;
|
||||
user: null;
|
||||
}
|
||||
|
||||
interface Banners {
|
||||
displaySale: boolean;
|
||||
}
|
||||
|
||||
interface Corrections {
|
||||
items: null;
|
||||
data: null;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
bookPage: BookPage | null;
|
||||
chapterPage: ChapterPage | null;
|
||||
}
|
||||
|
||||
interface BookPage {
|
||||
bookId: number;
|
||||
bookmark: null;
|
||||
}
|
||||
|
||||
interface ChapterPage {
|
||||
chapterId: number;
|
||||
bookId: number;
|
||||
bookmark: null;
|
||||
}
|
||||
|
||||
interface ReaderSettings {
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
textIndent: number;
|
||||
sidebarHide: number;
|
||||
textAlign: string;
|
||||
}
|
||||
|
||||
interface TotalData {
|
||||
items: Item[];
|
||||
_links: Links;
|
||||
pagesData: PagesData;
|
||||
}
|
||||
|
||||
interface Links {
|
||||
self: First;
|
||||
first: First;
|
||||
last: First;
|
||||
next: First;
|
||||
}
|
||||
|
||||
interface First {
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
status: string;
|
||||
dislikes: number;
|
||||
likes: number;
|
||||
userRating: null;
|
||||
lastChapter: ChapterElement;
|
||||
verticalImage: Image;
|
||||
horizontalImage: Image;
|
||||
squareImage: Image;
|
||||
}
|
||||
|
||||
interface PagesData {
|
||||
totalCount: number;
|
||||
pageCount: number;
|
||||
currentPage: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
interface Query {
|
||||
book?: string;
|
||||
chapter?: string;
|
||||
}
|
||||
|
||||
+1081
-1105
File diff suppressed because it is too large
Load Diff
+286
-371
@@ -1,371 +1,286 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
class TuNovelaLigera implements Plugin.PluginBase {
|
||||
id = "tunovelaligera";
|
||||
name = "TuNovelaLigera";
|
||||
icon = "src/es/tunovelaligera/icon.png";
|
||||
site = "https://tunovelaligera.com/";
|
||||
version = "1.0.0";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}`;
|
||||
|
||||
link += filters.genres.value ? `genero/` + filters.genres.value : "novelas";
|
||||
|
||||
link += `/page/${pageNo}`;
|
||||
|
||||
link += filters.order.value;
|
||||
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(link, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".page-item-detail").each((i, el) => {
|
||||
const novelName = loadedCheerio(el).find(".h5 > a").text();
|
||||
const novelCover = loadedCheerio(el).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(el).find(".h5 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
loadedCheerio(".manga-title-badges").remove();
|
||||
|
||||
novel.name = loadedCheerio(".post-title > h1").text().trim();
|
||||
|
||||
let novelCover = loadedCheerio(".summary_image > a > img");
|
||||
|
||||
novel.cover =
|
||||
novelCover.attr("data-src") ||
|
||||
novelCover.attr("src") ||
|
||||
novelCover.attr("data-cfsrc") ||
|
||||
defaultCover;
|
||||
|
||||
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 "Generos":
|
||||
novel.genres = detail.replace(/, /g, ",");
|
||||
break;
|
||||
case "Autores":
|
||||
novel.author = detail;
|
||||
break;
|
||||
case "Estado":
|
||||
novel.status =
|
||||
detail.includes("OnGoing") ||
|
||||
detail.includes("Updating")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.summary = loadedCheerio("div.summary__content > p").text().trim();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
let lastPage = 1;
|
||||
lastPage = +loadedCheerio(".lcp_paginator li:last").prev().text();
|
||||
|
||||
const getChapters = async () => {
|
||||
const n = url.split("/");
|
||||
const novelName = n[4];
|
||||
const formData = new FormData();
|
||||
formData.append("action", "madara_load_more");
|
||||
formData.append("page", "0");
|
||||
formData.append("template", "html/loop/content");
|
||||
formData.append("vars[category_name]", novelName);
|
||||
formData.append("vars[posts_per_page]", "10000");
|
||||
|
||||
const result = await fetchApi(
|
||||
`${this.site}wp-admin/admin-ajax.php`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const text = await result.text();
|
||||
|
||||
loadedCheerio = parseHTML(text);
|
||||
|
||||
loadedCheerio(".heading").each((i, el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl =
|
||||
loadedCheerio(el).find("a").attr("href") || "";
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
url: chapterUrl,
|
||||
releaseTime: releaseDate,
|
||||
});
|
||||
});
|
||||
return chapter.reverse();
|
||||
};
|
||||
|
||||
const getPageChapters = async () => {
|
||||
for (let i = 1; i <= lastPage; i++) {
|
||||
const chaptersUrl = `${novelUrl}?lcp_page0=${i}`;
|
||||
const result = await fetchApi(chaptersUrl, { headers });
|
||||
const chaptersHTML = await result.text();
|
||||
|
||||
let chapterCheerio = parseHTML(chaptersHTML);
|
||||
|
||||
chapterCheerio(".lcp_catlist li").each((i, el) => {
|
||||
const chapterName = chapterCheerio(el)
|
||||
.find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
const releaseDate = chapterCheerio(el)
|
||||
.find("span")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const chapterUrl =
|
||||
chapterCheerio(el).find("a").attr("href") || "";
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
await delay(1000);
|
||||
}
|
||||
return chapter.reverse();
|
||||
};
|
||||
|
||||
novel.chapters = await getChapters();
|
||||
|
||||
if (!novel.chapters.length) {
|
||||
await delay(1000);
|
||||
novel.chapters = await getPageChapters();
|
||||
}
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#hola_siguiente").next().find("div").remove();
|
||||
const chapterText =
|
||||
loadedCheerio("#hola_siguiente").next().html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo?: number | undefined
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}?s=${searchTerm}&post_type=wp-manga`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".c-tabs-item__content").each((i, el) => {
|
||||
const novelName = loadedCheerio(el).find(".h4 > a").text();
|
||||
const novelCover = loadedCheerio(el).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(el).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 await fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
value: "?m_orderby=rating",
|
||||
label: "Ordenado por",
|
||||
options: [
|
||||
{ label: "Lo mas reciente", value: "?m_orderby=latest" },
|
||||
|
||||
{ label: "A-Z", value: "?m_orderby=alphabet" },
|
||||
|
||||
{ label: "Clasificación", value: "?m_orderby=rating" },
|
||||
|
||||
{ label: "Trending", value: "?m_orderby=trending" },
|
||||
|
||||
{ label: "Mas visto", value: "?m_orderby=views" },
|
||||
|
||||
{ label: "Nuevo", value: "?m_orderby=new-manga" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
value: "",
|
||||
label: "Generos",
|
||||
options: [
|
||||
{ label: "None", value: "" },
|
||||
{ label: "Acción", value: "accion" },
|
||||
|
||||
{ label: "Adulto", value: "adulto" },
|
||||
|
||||
{ label: "Artes Marciales", value: "artes-marciales" },
|
||||
|
||||
{ label: "Aventura", value: "aventura" },
|
||||
|
||||
{ label: "Ciencia Ficción", value: "ciencia-ficcion" },
|
||||
|
||||
{ label: "Comedia", value: "comedia" },
|
||||
|
||||
{ label: "Deportes", value: "deportes" },
|
||||
|
||||
{ label: "Drama", value: "drama" },
|
||||
|
||||
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
|
||||
|
||||
{ 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();
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
class TuNovelaLigera implements Plugin.PluginBase {
|
||||
id = "tunovelaligera";
|
||||
name = "TuNovelaLigera";
|
||||
icon = "src/es/tunovelaligera/icon.png";
|
||||
site = "https://tunovelaligera.com";
|
||||
version = "1.0.0";
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site
|
||||
|
||||
link += filters?.genres?.value ? "/genero" + filters.genres.value : "/novelas";
|
||||
link += `/page/${pageNo}?m_orderby=`;
|
||||
link += (showLatestNovels ? "latest" : filters?.order?.value || "rating");
|
||||
|
||||
const result = await fetchApi(link).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".page-item-detail").each((i, el) => {
|
||||
const name = loadedCheerio(el).find(".h5 > a").text();
|
||||
const cover = loadedCheerio(el).find("img").attr("src");
|
||||
const url = loadedCheerio(el).find(".h5 > a").attr("href");
|
||||
if (!url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
loadedCheerio(".manga-title-badges").remove();
|
||||
|
||||
novel.name = loadedCheerio(".post-title > h1").text().trim();
|
||||
|
||||
let novelCover = loadedCheerio(".summary_image > a > img");
|
||||
|
||||
novel.cover =
|
||||
novelCover.attr("data-src") ||
|
||||
novelCover.attr("src") ||
|
||||
novelCover.attr("data-cfsrc") ||
|
||||
defaultCover;
|
||||
|
||||
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 "Género(s)":
|
||||
novel.genres = detail.replace(/, /g, ",");
|
||||
break;
|
||||
case "Autor(es)":
|
||||
novel.author = detail;
|
||||
break;
|
||||
case "Estado":
|
||||
novel.status =
|
||||
detail.includes("OnGoing") || detail.includes("Updating")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.summary = loadedCheerio("div.summary__content > p").text().trim();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
let lastPage = 1;
|
||||
lastPage = +loadedCheerio(".lcp_paginator li:last").prev().text();
|
||||
|
||||
const getChapters = async () => {
|
||||
const n = novelUrl.split("/");
|
||||
const novelName = n[4];
|
||||
const formData = new FormData();
|
||||
formData.append("action", "madara_load_more");
|
||||
formData.append("page", "0");
|
||||
formData.append("template", "html/loop/content");
|
||||
formData.append("vars[category_name]", novelName);
|
||||
formData.append("vars[posts_per_page]", "10000");
|
||||
|
||||
const result = await fetchApi(this.site + '/wp-admin/admin-ajax.php', {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const text = await result.text();
|
||||
|
||||
loadedCheerio = parseHTML(text);
|
||||
|
||||
loadedCheerio(".heading").each((i, el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href") || "";
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
url: chapterUrl,
|
||||
releaseTime: releaseDate,
|
||||
});
|
||||
});
|
||||
return chapter.reverse();
|
||||
};
|
||||
|
||||
const getPageChapters = async () => {
|
||||
for (let i = 1; i <= lastPage; i++) {
|
||||
const chaptersUrl = `${novelUrl}?lcp_page0=${i}`;
|
||||
const result = await fetchApi(chaptersUrl);
|
||||
const chaptersHTML = await result.text();
|
||||
|
||||
let chapterCheerio = parseHTML(chaptersHTML);
|
||||
|
||||
chapterCheerio("#lcp_instance_0 li").each((i, el) => {
|
||||
const chapterName = chapterCheerio(el)
|
||||
.find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
const releaseDate = chapterCheerio(el).find("span").text().trim();
|
||||
|
||||
const chapterUrl = chapterCheerio(el).find("a").attr("href") || "";
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
await delay(1000);
|
||||
}
|
||||
return chapter.reverse();
|
||||
};
|
||||
|
||||
novel.chapters = await getChapters();
|
||||
|
||||
if (!novel.chapters.length) {
|
||||
await delay(1000);
|
||||
novel.chapters = await getPageChapters();
|
||||
}
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#hola_siguiente").next().find("div").remove();
|
||||
const chapterText = loadedCheerio("#hola_siguiente").next().html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/?s=${searchTerm}&post_type=wp-manga`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".c-tabs-item__content").each((i, el) => {
|
||||
const name = loadedCheerio(el).find(".h4 > a").text();
|
||||
const cover = loadedCheerio(el).find("img").attr("src");
|
||||
const url = loadedCheerio(el).find(".h4 > a").attr("href");
|
||||
if (!url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
value: "rating",
|
||||
label: "Ordenado por",
|
||||
options: [
|
||||
{ label: "Lo mas reciente", value: "latest" },
|
||||
{ label: "A-Z", value: "alphabet" },
|
||||
{ label: "Clasificación", value: "rating" },
|
||||
{ label: "Trending", value: "trending" },
|
||||
{ label: "Mas visto", value: "views" },
|
||||
{ label: "Nuevo", value: "new-manga" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
value: "",
|
||||
label: "Generos",
|
||||
options: [
|
||||
{ label: "None", value: "" },
|
||||
{ label: "Acción", value: "accion" },
|
||||
{ label: "Adulto", value: "adulto" },
|
||||
{ label: "Artes Marciales", value: "artes-marciales" },
|
||||
{ label: "Aventura", value: "aventura" },
|
||||
{ label: "Ciencia Ficción", value: "ciencia-ficcion" },
|
||||
{ label: "Comedia", value: "comedia" },
|
||||
{ label: "Deportes", value: "deportes" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
|
||||
{ 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();
|
||||
|
||||
Reference in New Issue
Block a user