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:
+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
Reference in New Issue
Block a user