Full TS for Plugins (#538)

* remove lang, descrription in plugin script

* remove languages import

* Update README.md

* fix: missing lang

* clean: remove description

* feat(Add Sources) (#15)

* add many sources

* add showToast

* feat(Add Source): (#16)

* add source

* remove descriptions

* Early Novel & Fast Novel (#1)

* some more plugin

* plugin

* try something new

* yes

* try yes

* genres is plural

* fastnovel full fix

* testings

* early check

* earlynovel fixed

* showToast check

* earlynovel done

* fastnovel fixe

* again

* showToasts again

* fix for fastnovel

* done

* for pr

* Update freenovelupdates.js

* MTL Reader and MTL Novel (#2)

* first

* test

* wops

* mtlreaders

* ok

* for PR

* Nitro Scans and Novel Hold (#3)

* start feature

* more feature

* something

* eslint

* yes

* maybe

* wa

* asdasd

* amazing

* change it up

* for pr

* WPmangastream and Novelfull (#4) (#17)

* source generation

* fix json parse error

* update plguins

* delete fetchApi, nobleMTL (.js)

* delete showToast.js

* Typescript Support (#18)

* Initial TS Config

* Initial TS Config

* Fix most errors

* Fix test webserver

* Fix webserver isPlugin not working

* Fix typos

* Add check if plugins have all the fields

* Move exports.protected to the bottom of the plugin

* Change username, reponame and branchname

* Fix no exports error

* Fix type

* Fix typo in tsc_fix

* Remove const from exports to not re-define consts

* Readd const becuause exports may not exist.

* Fix plugins to work in-app

* Fix NobleMTL

* Remove fix_ts.ts

* Change template.ts

* delete cheerio_space

* move api into ./testweb

* remove js dir

* remove js dir (again)

* remove ./js (again)

* init multisrc generator (test)

* remove module.exports in libs

* madara generator

* update template

* rename generator, TS for hako

* fix icon url for madara

* fix icon for madara (again)

* update Readme, nodemon

* export default for all libs

* fix madara novelurl

* update: pluginId, name, filename for madara

* feat: check same id error

* update: precommit, fix: require.default

* Revert "update: precommit, fix: require.default"

This reverts commit c642c5c72241f13eccfe417210c22272c4739ffa.

* Port all plugins to ts (#19)

- fix cheerio
- remove export default
- fix spelling

* update template

* add multisrc noveltl, update generator param, fix web search

* fix undefined id for Snailulitka

* add rulate multisrc, fix max length textarea

* add readwn multisrc

* delete: erolate and rulate are in multisrc

* Change broken extension and show error on commit for those files (#20)

* Change to .broken.ts and show error on commit for those files

* remove novelStatus

* fix: wrong id in github action

* remove mangastream multisrc

* update icon for readwn multisrc

---------

Co-authored-by: K1ngfish3r <trulyihu@gmail.com>
Co-authored-by: Marcin Majewski <skilletssgames@gmail.com>
This commit is contained in:
nyagami
2023-08-17 10:19:45 +07:00
committed by GitHub
parent e444031c25
commit e602df44c8
306 changed files with 58843 additions and 14008 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+341
View File
@@ -0,0 +1,341 @@
import cheerio from "cheerio";
import dayjs from "dayjs";
import { fetchApi } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { Chapter, Novel, Plugin } from "@typings/plugin";
import { defaultCover } from "@libs/defaultCover";
export const id = "AT";
export const name = "Автор Тудей";
export const site = "https://author.today";
export const version = "1.0.0";
export const icon = "src/ru/authortoday/icon.png";
const baseUrl = site;
const apiUrl = "https://api.author.today/";
const token = "Bearer guest";
export const popularNovels: Plugin.popularNovels = async function (
page,
{ showLatestNovels, filters }
) {
let url = apiUrl + "v1/catalog/search?page=" + page;
if (filters?.genre) {
url += "&genre=" + filters.genre;
}
url +=
"&sorting=" +
(showLatestNovels ? "recent" : filters?.sort || "popular");
url += "&form=" + (filters?.form || "any");
url += "&state=" + (filters?.state || "any");
url += "&series=" + (filters?.series || "any");
url += "&access=" + (filters?.access || "any");
url += "&promo=" + (filters?.promo || "hide");
const result = await fetchApi(url, {
headers: {
Authorization: token,
},
});
const json = await result.json();
if (json?.code === "NotFound") {
return [];
}
let novels: Novel.Item[] = [];
json.searchResults.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.coverUrl
? "https://cm.author.today/content/" + novel.coverUrl
: defaultCover,
url: baseUrl + "/work/" + novel.id,
})
);
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const workID = novelUrl.split("/")[4];
let result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, {
headers: {
Authorization: token,
},
});
let json = await result.json();
let novel: Novel.instance = {
url: novelUrl,
name: json.title,
cover: json?.coverUrl ? json.coverUrl.split("?")[0] : defaultCover,
author:
json?.originalAuthor ||
json?.authorFIO ||
json?.coAuthorFIO ||
json?.secondCoAuthorFIO ||
json?.translator ||
"",
genres: json?.tags?.join(", "),
status: json?.isFinished
? NovelStatus.Completed
: NovelStatus.Ongoing,
};
novel.summary = "";
novel.summary += json?.annotation ? json.annotation + "\n" : "";
novel.summary += json?.authorNotes
? "Примечания автора:\n" + json.authorNotes
: "";
// all chapters
result = await fetchApi(`${apiUrl}v1/work/${workID}/content`, {
headers: {
Authorization: token,
},
});
json = await result.json();
let chapters: Chapter.Item[] = [];
json?.forEach((chapter, index) => {
if (chapter?.isAvailable && !chapter?.isDraft) {
chapters.push({
name: chapter?.title || `Глава ${index + 1}`,
releaseTime: dayjs(
chapter?.publishTime || chapter.lastModificationTime
).format("LLL"),
url: `${apiUrl}v1/work/${workID}/chapter/${chapter.id}/text`,
});
}
});
novel.chapters = chapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi(chapterUrl, {
headers: {
Authorization: token,
},
});
const json = await result.json();
if (json?.code) {
return json.code + "\n" + json?.message;
}
let key = json.key.split("").reverse().join("") + "@_@";
let text = "";
for (let i = 0; i < json.text.length; i++) {
text += String.fromCharCode(
json.text.charCodeAt(i) ^ key.charCodeAt(Math.floor(i % key.length))
);
}
const loadedCheerio = cheerio.load(text);
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;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const url = `${baseUrl}/search?category=works&q=${searchTerm}`;
const result = await fetchApi(url, {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0",
referer: baseUrl,
});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels: Novel.Item[] = [];
loadedCheerio("div.book-row").each(function () {
const name = loadedCheerio(this)
.find('div[class="book-title"] a')
.text()
.trim();
let cover = loadedCheerio(this).find("img").attr("src");
const url =
baseUrl +
"/work/" +
loadedCheerio(this)
.find('div[class="book-title"] a')
.attr("href")
?.split("/")[2];
if (cover) {
cover = cover.split("?")[0];
} else {
cover = defaultCover;
}
novels.push({ name, cover, url });
});
return novels;
};
export const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "По популярности", value: "popular" },
{ label: "По количеству лайков", value: "likes" },
{ label: "По комментариям", value: "comments" },
{ label: "По новизне", value: "recent" },
{ label: "По просмотрам", value: "views" },
{ label: "Набирающие популярность", value: "trending" },
],
inputType: FilterInputs.Picker,
},
{
key: "genre",
label: "Жанры",
values: [
{ label: "Альтернативная история", value: "sf-history" },
{ label: "Антиутопия", value: "dystopia" },
{ label: "Бизнес-литература", value: "biznes-literatura" },
{ label: "Боевая фантастика", value: "sf-action" },
{ label: "Боевик", value: "action" },
{ label: "Боевое фэнтези", value: "fantasy-action" },
{ label: "Бояръ-Аниме", value: "boyar-anime" },
{ label: "Героическая фантастика", value: "sf-heroic" },
{ label: "Героическое фэнтези", value: "heroic-fantasy" },
{ label: "Городское фэнтези", value: "urban-fantasy" },
{ label: "Детектив", value: "detective" },
{ label: "Детская литература", value: "detskaya-literatura" },
{ label: "Документальная проза", value: "non-fiction" },
{ label: "Историческая проза", value: "historical-fiction" },
{
label: "Исторические приключения",
value: "historical-adventure",
},
{ label: "Исторический детектив", value: "historical-mystery" },
{
label: "Исторический любовный роман",
value: "historical-romance",
},
{ label: "Историческое фэнтези", value: "historical-fantasy" },
{ label: "Киберпанк", value: "cyberpunk" },
{ label: "Короткий любовный роман", value: "short-romance" },
{ label: "Космическая фантастика", value: "sf-space" },
{ label: "ЛитРПГ", value: "litrpg" },
{ label: "Любовное фэнтези", value: "love-fantasy" },
{ label: "Любовные романы", value: "romance" },
{ label: "Мистика", value: "paranormal" },
{ label: "Назад в СССР", value: "back-to-ussr" },
{ label: "Научная фантастика", value: "science-fiction" },
{ label: "Подростковая проза", value: "teen-prose" },
{ label: "Политический роман", value: "political-fiction" },
{ label: "Попаданцы", value: "popadantsy" },
{ label: "Попаданцы в космос", value: "popadantsy-v-kosmos" },
{
label: "Попаданцы в магические миры",
value: "popadantsy-v-magicheskie-miry",
},
{ label: "Попаданцы во времени", value: "popadantsy-vo-vremeni" },
{ label: "Постапокалипсис", value: "postapocalyptic" },
{ label: "Поэзия", value: "poetry" },
{ label: "Приключения", value: "adventure" },
{ label: "Публицистика", value: "publicism" },
{ label: "Развитие личности", value: "razvitie-lichnosti" },
{ label: "Разное", value: "other" },
{ label: "РеалРПГ", value: "realrpg" },
{ label: "Романтическая эротика", value: "romantic-erotika" },
{ label: "Сказка", value: "fairy-tale" },
{ label: "Современная проза", value: "modern-prose" },
{
label: "Современный любовный роман",
value: "contemporary-romance",
},
{ label: "Социальная фантастика", value: "sf-social" },
{ label: "Стимпанк", value: "steampunk" },
{ label: "Темное фэнтези", value: "dark-fantasy" },
{ label: "Триллер", value: "thriller" },
{ label: "Ужасы", value: "horror" },
{ label: "Фантастика", value: "sci-fi" },
{
label: "Фантастический детектив",
value: "detective-science-fiction",
},
{ label: "Фанфик", value: "fanfiction" },
{ label: "Фэнтези", value: "fantasy" },
{ label: "Шпионский детектив", value: "spy-mystery" },
{ label: "Эпическое фэнтези", value: "epic-fantasy" },
{ label: "Эротика", value: "erotica" },
{ label: "Эротическая фантастика", value: "sf-erotika" },
{ label: "Эротический фанфик", value: "fanfiction-erotika" },
{ label: "Эротическое фэнтези", value: "fantasy-erotika" },
{ label: "Юмор", value: "humor" },
{ label: "Юмористическая фантастика", value: "sf-humor" },
{ label: "Юмористическое фэнтези", value: "ironical-fantasy" },
],
inputType: FilterInputs.Picker,
},
{
key: "form",
label: "Форма произведения",
values: [
{ label: "Любой", value: "any" },
{ label: "Перевод", value: "translation" },
{ label: "Повесть", value: "tale" },
{ label: "Рассказ", value: "story" },
{ label: "Роман", value: "novel" },
{ label: "Сборник поэзии", value: "poetry" },
{ label: "Сборник рассказов", value: "story-book" },
],
inputType: FilterInputs.Picker,
},
{
key: "state",
label: "Статус произведения",
values: [
{ label: "Любой статус", value: "any" },
{ label: "В процессе", value: "in-progress" },
{ label: "Завершено", value: "finished" },
],
inputType: FilterInputs.Picker,
},
{
key: "series",
label: "Статус цикла",
values: [
{ label: "Не важно", value: "any" },
{ label: "Вне цикла", value: "out" },
{ label: "Цикл завершен", value: "finished" },
{ label: "Цикл не завершен", value: "unfinished" },
],
inputType: FilterInputs.Picker,
},
{
key: "access",
label: "Тип доступа",
values: [
{ label: "Любой", value: "any" },
{ label: "Платный", value: "paid" },
{ label: "Бесплатный", value: "free" },
],
inputType: FilterInputs.Picker,
},
{
key: "promo",
label: "Промо-фрагмент",
values: [
{ label: "Скрывать", value: "hide" },
{ label: "Показывать", value: "show" },
],
inputType: FilterInputs.Picker,
},
];
-336
View File
@@ -1,336 +0,0 @@
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const defaultCoverUri =
"https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true";
const pluginId = "AT";
const sourceName = "Автор Тудей";
const baseUrl = "https://author.today";
const apiUrl = "https://api.author.today/";
const token = "Bearer guest";
async function popularNovels(page, { showLatestNovels, filters }) {
let url = apiUrl + "v1/catalog/search?page=" + page;
if (filters?.genre) {
url += "&genre=" + filters.genre;
}
url +=
"&sorting=" + (showLatestNovels ? "recent" : filters?.sort || "popular");
url += "&form=" + (filters?.form || "any");
url += "&state=" + (filters?.state || "any");
url += "&series=" + (filters?.series || "any");
url += "&access=" + (filters?.access || "any");
url += "&promo=" + (filters?.promo || "hide");
const result = await fetchApi(url, {
headers: {
Authorization: token,
},
});
const json = await result.json();
if (json?.code === "NotFound") {
return [];
}
let novels = [];
json.searchResults.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.coverUrl
? "https://cm.author.today/content/" + novel.coverUrl
: defaultCoverUri,
url: baseUrl + "/work/" + novel.id,
})
);
return novels;
}
async function parseNovelAndChapters(novelUrl) {
const workID = novelUrl.split("/")[4];
let result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, {
headers: {
Authorization: token,
},
});
let json = await result.json();
let novel = {
url: novelUrl,
name: json.title,
cover: json?.coverUrl ? json.coverUrl.split("?")[0] : defaultCoverUri,
author:
json?.originalAuthor ||
json?.authorFIO ||
json?.coAuthorFIO ||
json?.secondCoAuthorFIO ||
json?.translator ||
"",
genres: json?.tags?.join(", "),
status: json?.isFinished ? Status.Completed : Status.Ongoing,
};
novel.summary = "";
novel.summary += json?.annotation ? json.annotation + "\n" : "";
novel.summary += json?.authorNotes
? "Примечания автора:\n" + json.authorNotes
: "";
// all chapters
result = await fetchApi(`${apiUrl}v1/work/${workID}/content`, {
headers: {
Authorization: token,
},
});
json = await result.json();
let chapters = [];
json?.forEach((chapter, index) => {
if (chapter?.isAvailable && !chapter?.isDraft) {
chapters.push({
name: chapter?.title || `Глава ${index + 1}`,
releaseTime: dayjs(
chapter?.publishTime || chapter.lastModificationTime
).format("LLL"),
url: `${apiUrl}v1/work/${workID}/chapter/${chapter.id}/text`,
});
}
});
novel.chapters = chapters;
return novel;
}
async function parseChapter(chapterUrl) {
const result = await fetchApi(chapterUrl, {
headers: {
Authorization: token,
},
});
const json = await result.json();
if (json?.code) {
return json.code + "\n" + json?.message;
}
let key = json.key.split("").reverse().join("") + "@_@";
let text = "";
for (let i = 0; i < json.text.length; i++) {
text += String.fromCharCode(
json.text.charCodeAt(i) ^ key.charCodeAt(Math.floor(i % key.length))
);
}
const loadedCheerio = cheerio.load(text);
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 function searchNovels(searchTerm) {
const url = `${baseUrl}/search?category=works&q=${searchTerm}`;
const result = await fetchApi(url, {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0",
referer: baseUrl,
});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio("div.book-row").each(function () {
const name = loadedCheerio(this)
.find('div[class="book-title"] a')
.text()
.trim();
let cover = loadedCheerio(this).find("img").attr("src");
const url =
baseUrl +
"/work/" +
loadedCheerio(this)
.find('div[class="book-title"] a')
.attr("href")
.split("/")[2];
if (cover) {
cover = cover.split("?")[0];
} else {
cover = defaultCoverUri;
}
novels.push({ name, cover, url });
});
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "По популярности", value: "popular" },
{ label: "По количеству лайков", value: "likes" },
{ label: "По комментариям", value: "comments" },
{ label: "По новизне", value: "recent" },
{ label: "По просмотрам", value: "views" },
{ label: "Набирающие популярность", value: "trending" },
],
inputType: FilterInputs.Picker,
},
{
key: "genre",
label: "Жанры",
values: [
{ label: "Альтернативная история", value: "sf-history" },
{ label: "Антиутопия", value: "dystopia" },
{ label: "Бизнес-литература", value: "biznes-literatura" },
{ label: "Боевая фантастика", value: "sf-action" },
{ label: "Боевик", value: "action" },
{ label: "Боевое фэнтези", value: "fantasy-action" },
{ label: "Бояръ-Аниме", value: "boyar-anime" },
{ label: "Героическая фантастика", value: "sf-heroic" },
{ label: "Героическое фэнтези", value: "heroic-fantasy" },
{ label: "Городское фэнтези", value: "urban-fantasy" },
{ label: "Детектив", value: "detective" },
{ label: "Детская литература", value: "detskaya-literatura" },
{ label: "Документальная проза", value: "non-fiction" },
{ label: "Историческая проза", value: "historical-fiction" },
{ label: "Исторические приключения", value: "historical-adventure" },
{ label: "Исторический детектив", value: "historical-mystery" },
{ label: "Исторический любовный роман", value: "historical-romance" },
{ label: "Историческое фэнтези", value: "historical-fantasy" },
{ label: "Киберпанк", value: "cyberpunk" },
{ label: "Короткий любовный роман", value: "short-romance" },
{ label: "Космическая фантастика", value: "sf-space" },
{ label: "ЛитРПГ", value: "litrpg" },
{ label: "Любовное фэнтези", value: "love-fantasy" },
{ label: "Любовные романы", value: "romance" },
{ label: "Мистика", value: "paranormal" },
{ label: "Назад в СССР", value: "back-to-ussr" },
{ label: "Научная фантастика", value: "science-fiction" },
{ label: "Подростковая проза", value: "teen-prose" },
{ label: "Политический роман", value: "political-fiction" },
{ label: "Попаданцы", value: "popadantsy" },
{ label: "Попаданцы в космос", value: "popadantsy-v-kosmos" },
{
label: "Попаданцы в магические миры",
value: "popadantsy-v-magicheskie-miry",
},
{ label: "Попаданцы во времени", value: "popadantsy-vo-vremeni" },
{ label: "Постапокалипсис", value: "postapocalyptic" },
{ label: "Поэзия", value: "poetry" },
{ label: "Приключения", value: "adventure" },
{ label: "Публицистика", value: "publicism" },
{ label: "Развитие личности", value: "razvitie-lichnosti" },
{ label: "Разное", value: "other" },
{ label: "РеалРПГ", value: "realrpg" },
{ label: "Романтическая эротика", value: "romantic-erotika" },
{ label: "Сказка", value: "fairy-tale" },
{ label: "Современная проза", value: "modern-prose" },
{ label: "Современный любовный роман", value: "contemporary-romance" },
{ label: "Социальная фантастика", value: "sf-social" },
{ label: "Стимпанк", value: "steampunk" },
{ label: "Темное фэнтези", value: "dark-fantasy" },
{ label: "Триллер", value: "thriller" },
{ label: "Ужасы", value: "horror" },
{ label: "Фантастика", value: "sci-fi" },
{ label: "Фантастический детектив", value: "detective-science-fiction" },
{ label: "Фанфик", value: "fanfiction" },
{ label: "Фэнтези", value: "fantasy" },
{ label: "Шпионский детектив", value: "spy-mystery" },
{ label: "Эпическое фэнтези", value: "epic-fantasy" },
{ label: "Эротика", value: "erotica" },
{ label: "Эротическая фантастика", value: "sf-erotika" },
{ label: "Эротический фанфик", value: "fanfiction-erotika" },
{ label: "Эротическое фэнтези", value: "fantasy-erotika" },
{ label: "Юмор", value: "humor" },
{ label: "Юмористическая фантастика", value: "sf-humor" },
{ label: "Юмористическое фэнтези", value: "ironical-fantasy" },
],
inputType: FilterInputs.Picker,
},
{
key: "form",
label: "Форма произведения",
values: [
{ label: "Любой", value: "any" },
{ label: "Перевод", value: "translation" },
{ label: "Повесть", value: "tale" },
{ label: "Рассказ", value: "story" },
{ label: "Роман", value: "novel" },
{ label: "Сборник поэзии", value: "poetry" },
{ label: "Сборник рассказов", value: "story-book" },
],
inputType: FilterInputs.Picker,
},
{
key: "state",
label: "Статус произведения",
values: [
{ label: "Любой статус", value: "any" },
{ label: "В процессе", value: "in-progress" },
{ label: "Завершено", value: "finished" },
],
inputType: FilterInputs.Picker,
},
{
key: "series",
label: "Статус цикла",
values: [
{ label: "Не важно", value: "any" },
{ label: "Вне цикла", value: "out" },
{ label: "Цикл завершен", value: "finished" },
{ label: "Цикл не завершен", value: "unfinished" },
],
inputType: FilterInputs.Picker,
},
{
key: "access",
label: "Тип доступа",
values: [
{ label: "Любой", value: "any" },
{ label: "Платный", value: "paid" },
{ label: "Бесплатный", value: "free" },
],
inputType: FilterInputs.Picker,
},
{
key: "promo",
label: "Промо-фрагмент",
values: [
{ label: "Скрывать", value: "hide" },
{ label: "Показывать", value: "show" },
],
inputType: FilterInputs.Picker,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/authortoday/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "axel.znovel";
export const name = "Аксел";
export const icon = "";
export const version = "1.0.0";
export const site = "https://axel.znovel.space";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Богини","value":316},{"label":"Везучий главный герой","value":411},{"label":"Виртуальная реальность","value":744},{"label":"Высокомерный герой","value":56},{"label":"Герой-извращенец","value":521},{"label":"Главная героиня — женщина","value":277},{"label":"Главный герой — мужчина","value":419},{"label":"Дворяне","value":487},{"label":"Девочки-волшебницы","value":415},{"label":"Демоны","value":193},{"label":"Дружба","value":293},{"label":"Есть аниме-адаптация","value":10},{"label":"Есть видеоигра","value":13},{"label":"Есть манга-адаптация","value":14},{"label":"Игра на выживание","value":696},{"label":"Из грязи в князи","value":542},{"label":"Королевская особа","value":596},{"label":"Ленивый главный герой","value":389},{"label":"Маги","value":757},{"label":"Магические технологии","value":417}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "bel.novel";
export const name = "Белманга";
export const icon = "";
export const version = "1.0.0";
export const site = "https://bel.novel.tl";
export const filters: Filter[] = [];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+226
View File
@@ -0,0 +1,226 @@
import cheerio from "cheerio";
import dayjs from "dayjs";
import { fetchApi } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { Chapter, Novel, Plugin } from "@typings/plugin";
export const id = "bookriver";
export const name = "Bookriver";
export const site = "https://bookriver.ru";
export const version = "1.0.0";
export const icon = "src/ru/bookriver/icon.png";
const baseUrl = site;
export const popularNovels: Plugin.popularNovels = async function (
page,
{ showLatestNovels, filters }
) {
let url = baseUrl + `/genre?page=${page}&perPage=24&sortingType=`;
url += showLatestNovels ? "last-update" : filters?.sort || "bestseller";
if (filters) {
if (Array.isArray(filters.genres) && filters.genres.length) {
url += "&g=" + filters.genres.join(",");
}
}
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let novels: Novel.Item[] = [];
json.props.pageProps.state.pagesFilter.genre.books.forEach((novel) =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
url: baseUrl + "/book/" + novel.slug,
})
);
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const json = loadedCheerio("#__NEXT_DATA__").html();
const book = JSON.parse(json).props.pageProps.state.book.bookPage;
let novel: Novel.instance = {
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,
};
let chapters: Chapter.Item[] = [];
book.ebook.chapters.forEach((chapter) => {
if (chapter.available) {
chapters.push({
name: chapter.name,
releaseTime: dayjs(
chapter?.firstPublishedAt || chapter.createdAt
).format("LLL"),
url:
baseUrl +
"/reader/" +
book.slug +
"/" +
chapter.chapterId,
});
}
});
novel.chapters = chapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const url = "https://api.bookriver.ru/api/v1/books/chapter/text/";
const result = await fetchApi(url + chapterUrl.split("/").pop());
const json = await result.json();
const chapterText = json.data.content;
return chapterText;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const url = `${baseUrl}/search/books?keyword=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let novels: Novel.Item[] = [];
json.props.pageProps.state.catalog.books.books.forEach((novel) =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
url: baseUrl + "/book/" + novel.slug,
})
);
return novels;
};
export const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Бестселлеры", value: "bestseller" },
{ label: "Дате добавления", value: "newest" },
{ label: "Дате обновления", value: "last-update" },
],
inputType: FilterInputs.Picker,
},
{
key: "genres",
label: "жанры",
values: [
{
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",
},
],
inputType: FilterInputs.Checkbox,
},
];
-184
View File
@@ -1,184 +0,0 @@
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const pluginId = "bookriver";
const sourceName = "Bookriver";
const baseUrl = "https://bookriver.ru";
async function popularNovels(page, { showLatestNovels, filters }) {
let url = baseUrl + `/genre?page=${page}&perPage=24&sortingType=`;
url += showLatestNovels ? "last-update" : filters?.sort || "bestseller";
if (filters?.genres?.length) {
url += "&g=" + filters.genres.join(",");
}
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let novels = [];
json.props.pageProps.state.pagesFilter.genre.books.forEach((novel) =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
url: baseUrl + "/book/" + novel.slug,
})
);
return novels;
}
async function parseNovelAndChapters(novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const json = loadedCheerio("#__NEXT_DATA__").html();
const book = JSON.parse(json).props.pageProps.state.book.bookPage;
let novel = {
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" ? Status.Ongoing : Status.Completed,
};
let chapters = [];
book.ebook.chapters.forEach((chapter) => {
if (chapter.available) {
chapters.push({
name: chapter.name,
releaseTime: dayjs(chapter?.firstPublishedAt || chapter.createdAt).format("LLL"),
url: baseUrl + "/reader/" + book.slug + "/" + chapter.chapterId,
});
}
});
novel.chapters = chapters;
return novel;
}
async function parseChapter(chapterUrl) {
const url = "https://api.bookriver.ru/api/v1/books/chapter/text/";
const result = await fetchApi(url + chapterUrl.split("/").pop());
const json = await result.json();
const chapterText = json.data.content;
return chapterText;
}
async function searchNovels(searchTerm) {
const url = `${baseUrl}/search/books?keyword=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let novels = [];
json.props.pageProps.state.catalog.books.books.forEach((novel) =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
url: baseUrl + "/book/" + novel.slug,
})
);
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Бестселлеры", value: "bestseller" },
{ label: "Дате добавления", value: "newest" },
{ label: "Дате обновления", value: "last-update" },
],
inputType: FilterInputs.Picker,
},
{
key: "genres",
label: "жанры",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/bookriver/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
-702
View File
@@ -1,702 +0,0 @@
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const defaultCoverUri =
"https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true";
const pluginId = "EROLATE";
const sourceName = "Erolate";
const baseUrl = "https://erolate.com";
async function popularNovels(page, { showLatestNovels, filters }) {
let url = baseUrl + "/search?t=&cat=2";
url += "&sort=" + showLatestNovels ? "4" : filters?.sort || "6";
url += "&type=" + filters?.type || "0";
url += "&atmosphere=" + filters?.atmosphere || "0";
url += "&adult=" + filters?.adult || "0";
if (filters?.genres?.length) {
url += filters.genres.map((i) => `&genres[]=${i}`).join("");
}
if (filters?.tags?.length) {
url += filters.tags.map((i) => `&tags[]=${i}`).join("");
}
if (filters?.trash?.length) {
url += filters.trash.map((i) => `&${i}=1`);
}
url += `&Book_page=${page}`;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio(
'ul[class="search-results"] > li:not([class="ad_type_catalog"])'
).each(function () {
novels.push({
name: loadedCheerio(this).find("p > a").text(),
cover: baseUrl + loadedCheerio(this).find("img").attr("src"),
url: baseUrl + loadedCheerio(this).find("p > a").attr("href"),
});
});
return novels;
}
async function parseNovelAndChapters(novelUrl) {
let result = await fetchApi(novelUrl);
if (result.url.includes("mature?path=")) {
const formData = new FormData();
formData.append("path", novelUrl);
formData.append("ok", "Да");
await fetchApi(result.url, {
method: "POST",
body: formData,
});
result = await fetchApi(novelUrl);
}
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel = {
url: novelUrl,
};
novel.name = loadedCheerio('div[class="container"] > div > div > h1')
.text()
.trim();
novel.cover =
baseUrl + loadedCheerio('div[class="images"] > div img').attr("src");
novel.summary = loadedCheerio("#Info > div:nth-child(3)").html();
let genre = [];
loadedCheerio('div[class="span5"] > p').each(function () {
switch (loadedCheerio(this).find("strong").text()) {
case "Автор:":
novel.author = loadedCheerio(this).find("em > a").text().trim();
break;
case "Выпуск:":
novel.status =
loadedCheerio(this).find("em").text().trim() === "продолжается"
? Status.Ongoing
: Status.Completed;
break;
case "Тэги:":
loadedCheerio(this)
.find("em > a")
.each(function () {
genre.push(loadedCheerio(this).text());
});
break;
case "Жанры:":
loadedCheerio(this)
.find("em > a")
.each(function () {
genre.push(loadedCheerio(this).text());
});
break;
}
});
if (genre?.length) {
novel.genres = genre.reverse().join(",");
}
let chapters = [];
loadedCheerio("table > tbody > tr.chapter_row").each(function () {
const releaseTime = loadedCheerio(this)
.find("td > span")
.attr("title")
?.trim();
if (
loadedCheerio(this).find('td > span[class="disabled"]').length < 1 &&
releaseTime
) {
const name = loadedCheerio(this).find('td[class="t"] > a').text().trim();
const url = loadedCheerio(this).find('td[class="t"] > a').attr("href");
chapters.push({ name, releaseTime, url: baseUrl + url });
}
});
novel.chapters = chapters;
return novel;
}
async function parseChapter(chapterUrl) {
let result = await fetchApi(chapterUrl);
if (result.url.includes("mature?path=")) {
const formData = new FormData();
formData.append("path", chapterUrl.replace(baseUrl, ""));
formData.append("ok", "Да");
await fetchApi(result.url, {
method: "POST",
body: formData,
});
result = await fetchApi(chapterUrl);
}
const body = await result.text();
const loadedCheerio = cheerio.load(body);
loadedCheerio(".content-text 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(".content-text").html();
return chapterText;
}
async function searchNovels(searchTerm) {
const result = await fetchApi(
baseUrl + "/search/autocomplete?query=" + searchTerm
);
const json = await result.json();
let novels = [];
json.forEach((novel) =>
novels.push({
name: novel.title_one + " / " + novel.title_two,
cover: baseUrl + novel.img,
url: baseUrl + novel.url,
})
);
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "По рейтингу", value: "6" },
{ label: "По степени готовности", value: "0" },
{ label: "По названию на языке оригинала", value: "1" },
{ label: "По названию на языке перевода", value: "2" },
{ label: "По дате создания", value: "3" },
{ label: "По дате последней активности", value: "4" },
{ label: "По просмотрам", value: "5" },
{ label: "По кол-ву переведённых глав", value: "7" },
{ label: "По кол-ву лайков", value: "8" },
{ label: "По кол-ву страниц", value: "10" },
{ label: "По кол-ву бесплатных глав", value: "11" },
{ label: "По кол-ву рецензий", value: "12" },
{ label: "По кол-ву в закладках", value: "13" },
{ label: "По кол-ву в избранном", value: "14" },
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Неважно", value: "0" },
{ label: "Только переводы", value: "1" },
{ label: "Только авторские", value: "2" },
],
inputType: FilterInputs.Picker,
},
{
key: "atmosphere",
label: "Атмосфера",
values: [
{ label: "Неважно", value: "0" },
{ label: "Позитивная", value: "1" },
{ label: "Dark", value: "2" },
],
inputType: FilterInputs.Picker,
},
{
key: "trash",
label: "Другое",
values: [
{ label: "Готовые на 100%", value: "ready" },
{ label: "Доступные для перевода", value: "tr" },
{ label: "Доступные для скачивания", value: "gen" },
{ label: "Завершённые проекты", value: "wealth" },
{ label: "Распродажа", value: "discount" },
{ label: "Только онгоинги", value: "ongoings" },
{ label: "Убрать машинный", value: "remove_machinelate" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ label: "Анал", value: "2" },
{ label: "Бдсм", value: "3" },
{ label: "Большая грудь", value: "5" },
{ label: "Большая попка", value: "6" },
{ label: "Большой член", value: "7" },
{ label: "Бондаж", value: "8" },
{ label: "В первый раз", value: "9" },
{ label: "В цвете", value: "10" },
{ label: "Гарем", value: "11" },
{ label: "Гендарная интрига", value: "12" },
{ label: "Групповой секс", value: "13" },
{ label: "Детектив", value: "39" },
{ label: "Драма", value: "14" },
{ label: "Зрелые женщины (milf)", value: "15" },
{ label: "Измена", value: "16" },
{ label: "Изнасилование", value: "17" },
{ label: "Инцест", value: "18" },
{ label: "Исторический", value: "19" },
{ label: "Комедия", value: "20" },
{ label: "Контроль над разумом", value: "35" },
{ label: "Маленькая грудь", value: "21" },
{ label: "Мистика", value: "40" },
{ label: "Научная фантастика", value: "22" },
{ label: "Нетораре", value: "23" },
{ label: "Оральный секс", value: "24" },
{ label: "Повседневность", value: "41" },
{ label: "Приключения", value: "38" },
{ label: "Публичный секс", value: "33" },
{ label: "Романтика", value: "25" },
{ label: "С изображениями ", value: "36" },
{ label: "Сверхъестественное", value: "34" },
{ label: "Смат", value: "37" },
{ label: "Тентакли", value: "26" },
{ label: "Трагедия", value: "27" },
{ label: "Ужасы", value: "28" },
{ label: "Фантастика", value: "32" },
{ label: "Фэнтези", value: "29" },
{ label: "Чикан", value: "30" },
{ label: "Этти", value: "31" },
{ label: "Ahegao", value: "1" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "tags",
label: "Тэги",
values: [
{ label: "+18", value: "141" },
{ label: "18+", value: "200" },
{ label: "21+", value: "334" },
{ label: "Адекватные главные герои", value: "253" },
{ label: "Азартные игры", value: "210" },
{ label: "Айдолы", value: "364" },
{ label: "Акула", value: "53" },
{ label: "Альтернативное развитие событий", value: "338" },
{ label: "Аморальный главный герой", value: "219" },
{ label: "Анал", value: "7" },
{ label: "Анальный секс", value: "67" },
{ label: "Ангелы", value: "114" },
{ label: "Антигерой", value: "226" },
{ label: "Аристократия", value: "260" },
{ label: "Артефакт", value: "379" },
{ label: "Афродизиак", value: "276" },
{ label: "Ахегао", value: "240" },
{ label: "Бабушка беременна от внука", value: "393" },
{ label: "Бабушка и внук", value: "380" },
{ label: "Бдсм", value: "131" },
{ label: "Беременность", value: "148" },
{ label: "Бесплатно", value: "79" },
{ label: "Бесстрашные персонажи", value: "339" },
{ label: "Библиотека", value: "265" },
{ label: "Бистиалити", value: "391" },
{ label: "Близнецы", value: "198" },
{ label: "Блондинка", value: "91" },
{ label: "Богатые персонажи", value: "248" },
{ label: "Боги", value: "115" },
{ label: "Боевик", value: "252" },
{ label: "Большая грудь", value: "33" },
{ label: "Большая попка", value: "328" },
{ label: "Большой член", value: "76" },
{ label: "Большой член сына", value: "396" },
{ label: "Босс и подчиненный", value: "330" },
{ label: "Брак", value: "182" },
{ label: "Брак по расчёту", value: "346" },
{ label: "Брат", value: "29" },
{ label: "Брат и сестра", value: "30" },
{ label: "Брюнетка", value: "28" },
{ label: "Бэтмен", value: "369" },
{ label: "Бэтмен", value: "170" },
{ label: "В первый раз", value: "179" },
{ label: "Вагинальный секс", value: "57" },
{ label: "Вагинальный секс", value: "171" },
{ label: "Вайурист", value: "9" },
{ label: "Вампиры", value: "133" },
{ label: "Ван-пис", value: "335" },
{ label: "Веб камера", value: "289" },
{ label: "Ведьмы", value: "232" },
{ label: "Викинги", value: "307" },
{ label: "Виртуальная реальность", value: "382" },
{ label: "Вирус", value: "203" },
{ label: "Внучка", value: "189" },
{ label: "Война", value: "387" },
{ label: "Время", value: "81" },
{ label: "Второй шанс", value: "244" },
{ label: "Вуайеризм", value: "62" },
{ label: "Выживание", value: "135" },
{ label: "Гарем", value: "107" },
{ label: "Гарри поттер", value: "97" },
{ label: "Гг имба", value: "87" },
{ label: "Генетические модификации", value: "314" },
{ label: "Гипноз", value: "143" },
{ label: "Главная героиня девушка", value: "147" },
{ label: "Главный герой женщина", value: "213" },
{ label: "Главный герой извращенец", value: "116" },
{ label: "Главный герой мужчина", value: "117" },
{ label: "Глубокое горло", value: "94" },
{ label: "Гоблины", value: "230" },
{ label: "Город", value: "139" },
{ label: "Городское фэнтези", value: "297" },
{ label: "Грудное молоко", value: "225" },
{ label: "Групповой секс", value: "72" },
{ label: "Гэнг-бэнг", value: "235" },
{ label: "Гяру", value: "222" },
{ label: "Двойное проникновение", value: "42" },
{ label: "Двойной анал", value: "47" },
{ label: "Дворяне", value: "246" },
{ label: "Деамоны старший школы", value: "291" },
{ label: "Девственница", value: "119" },
{ label: "Девушки-монстры", value: "223" },
{ label: "Демоны", value: "98" },
{ label: "Детектив", value: "249" },
{ label: "Дзёсэй", value: "259" },
{ label: "Доктор", value: "326" },
{ label: "Дом", value: "138" },
{ label: "Доминирование", value: "18" },
{ label: "Дочь", value: "153" },
{ label: "Дочь беременна от отца", value: "348" },
{ label: "Древние времена", value: "383" },
{ label: "Древний китай", value: "261" },
{ label: "Другой мир", value: "365" },
{ label: "Дружба", value: "325" },
{ label: "Друзья детства", value: "183" },
{ label: "Дядя", value: "160" },
{ label: "Жена", value: "70" },
{ label: "Жена и муж", value: "99" },
{ label: "Жена шлюха", value: "236" },
{ label: "Женское доминирование", value: "100" },
{ label: "Жесткий секс", value: "283" },
{ label: "Жёсткий секс", value: "101" },
{ label: "Жестокие персонажи", value: "254" },
{ label: "Жестокий мир", value: "336" },
{ label: "Жестокость", value: "123" },
{ label: "Заботливый главный герой", value: "340" },
{ label: "Зависимость", value: "370" },
{ label: "Заключение", value: "196" },
{ label: "Заключённые", value: "384" },
{ label: "Замок", value: "140" },
{ label: "Запретная любовь", value: "273" },
{ label: "Заражение", value: "206" },
{ label: "Звёздные войны", value: "120" },
{ label: "Зверодевочки", value: "158" },
{ label: "Зло", value: "343" },
{ label: "Золовка", value: "77" },
{ label: "Золотой дождь", value: "146" },
{ label: "Зомби апокалипсис", value: "241" },
{ label: "Зоофилия", value: "144" },
{ label: "Игровые элементы", value: "385" },
{ label: "Извращения", value: "192" },
{ label: "Измена", value: "71" },
{ label: "Изменения внешности", value: "315" },
{ label: "Изменения личности", value: "316" },
{ label: "Изнасилование", value: "275" },
{ label: "Изуку мидория", value: "337" },
{ label: "Ино", value: "127" },
{ label: "Инопланетяне", value: "304" },
{ label: "Интересный сюжет", value: "349" },
{ label: "Интимные сцены", value: "322" },
{ label: "Интриги и заговоры", value: "262" },
{ label: "Интроверт", value: "38" },
{ label: "Инфекция", value: "207" },
{ label: "Инцест", value: "35" },
{ label: "Камера", value: "92" },
{ label: "Колледж", value: "82" },
{ label: "Комикс", value: "286" },
{ label: "Контроль", value: "19" },
{ label: "Контроль над разумом", value: "181" },
{ label: "Контроль разума", value: "65" },
{ label: "Кончил внутрь", value: "221" },
{ label: "Королевская семья", value: "358" },
{ label: "Коррупция", value: "49" },
{ label: "Космос", value: "204" },
{ label: "Красивая главная героиня", value: "255" },
{ label: "Красивые женщины", value: "321" },
{ label: "Красивый главный герой", value: "266" },
{ label: "Ксенофилия", value: "386" },
{ label: "Кулинария", value: "362" },
{ label: "Культвация", value: "86" },
{ label: "Кунилингус", value: "152" },
{ label: "Лактация", value: "11" },
{ label: "Лишение девственности", value: "212" },
{ label: "Лоли", value: "218" },
{ label: "Лунатизм", value: "279" },
{ label: "Любовь", value: "89" },
{ label: "Магический мир", value: "357" },
{ label: "Магия", value: "80" },
{ label: "Мама", value: "3" },
{ label: "Мама беременна от сына", value: "274" },
{ label: "Мама и дочь", value: "163" },
{ label: "Мама и сын", value: "1" },
{ label: "Мама супермена", value: "39" },
{ label: "Манипуляция временем", value: "292" },
{ label: "Марвел", value: "290" },
{ label: "Марвел", value: "173" },
{ label: "Марти стью", value: "102" },
{ label: "Массаж", value: "224" },
{ label: "Мастурбация", value: "63" },
{ label: "Мать", value: "239" },
{ label: "Мать и дочь", value: "381" },
{ label: "Мать и сын", value: "237" },
{ label: "Мафия", value: "256" },
{ label: "Мачеха", value: "197" },
{ label: "Мачеха беременна от сына", value: "395" },
{ label: "Мачеха и сын", value: "394" },
{ label: "Медсестра", value: "341" },
{ label: "Межрассовый", value: "54" },
{ label: "Месть", value: "306" },
{ label: "Меха", value: "268" },
{ label: "Ми", value: "243" },
{ label: "Милф", value: "34" },
{ label: "Милф", value: "180" },
{ label: "Минет", value: "52" },
{ label: "Мистика", value: "250" },
{ label: "Младшая сестра", value: "154" },
{ label: "Модель", value: "324" },
{ label: "Монстры", value: "333" },
{ label: "Мошеничество", value: "44" },
{ label: "Мужская беременность", value: "363" },
{ label: "Мужчина протагонист", value: "108" },
{ label: "Музыка", value: "331" },
{ label: "Мэри съюха", value: "309" },
{ label: "Наруто", value: "46" },
{ label: "Насилие", value: "128" },
{ label: "Насилие и жестокость", value: "193" },
{ label: "Научный эксперимент", value: "313" },
{ label: "Нежить", value: "373" },
{ label: "Некромантия", value: "298" },
{ label: "Ненормативная лексика", value: "124" },
{ label: "Нетораре", value: "69" },
{ label: "Нетори", value: "284" },
{ label: "Нижнее бельё", value: "392" },
{ label: "Ниндзя", value: "296" },
{ label: "Ношеные трусики", value: "166" },
{ label: "Нудизм", value: "156" },
{ label: "Няня", value: "88" },
{ label: "Обмен женами", value: "78" },
{ label: "Оборотни", value: "136" },
{ label: "Обратный гарем", value: "214" },
{ label: "Одержимость", value: "345" },
{ label: "Омегаверс", value: "368" },
{ label: "Оплодотворение", value: "157" },
{ label: "Оральный секс", value: "10" },
{ label: "Оргия", value: "356" },
{ label: "Орки", value: "388" },
{ label: "От слабого к сильному", value: "351" },
{ label: "Отец", value: "2" },
{ label: "Отец делится с сыном", value: "6" },
{ label: "Отец и дочь", value: "205" },
{ label: "Отец куколд", value: "272" },
{ label: "Офис", value: "234" },
{ label: "Падчерица", value: "271" },
{ label: "Пайзури", value: "195" },
{ label: "Папа и дочь", value: "360" },
{ label: "Паразиты", value: "208" },
{ label: "Параллельный мир", value: "320" },
{ label: "Пародия", value: "371" },
{ label: "Первый раз", value: "159" },
{ label: "Перемещение в другой мир", value: "347" },
{ label: "Перемещение во времени", value: "245" },
{ label: "Перерождение", value: "109" },
{ label: "Перерождение в злодея", value: "202" },
{ label: "Пирсинг", value: "145" },
{ label: "Писатель", value: "267" },
{ label: "Планомерное развитие событий", value: "350" },
{ label: "Повседневность", value: "247" },
{ label: "Подглядывание", value: "280" },
{ label: "Подчинение", value: "106" },
{ label: "Подчинение и унижение", value: "211" },
{ label: "Поедание киски", value: "26" },
{ label: "Покемоны", value: "132" },
{ label: "Покорная", value: "8" },
{ label: "Полигамия", value: "352" },
{ label: "Половое воспитание", value: "37" },
{ label: "Полулюди", value: "231" },
{ label: "Попаданец", value: "103" },
{ label: "Попадание в книгу", value: "361" },
{ label: "Порка", value: "20" },
{ label: "Порно", value: "278" },
{ label: "Постапокалипсис", value: "332" },
{ label: "Потеря девственности", value: "142" },
{ label: "Похищение", value: "161" },
{ label: "Приключения", value: "134" },
{ label: "Принуждение", value: "66" },
{ label: "Психология", value: "238" },
{ label: "Публично", value: "85" },
{ label: "Публичный секс", value: "233" },
{ label: "Раб", value: "353" },
{ label: "Рабы", value: "308" },
{ label: "Разват", value: "58" },
{ label: "Разврат", value: "64" },
{ label: "Райзен", value: "95" },
{ label: "Раса инопланетных космических лесбиянок", value: "294" },
{ label: "Рестленг", value: "84" },
{ label: "Риас гремори", value: "389" },
{ label: "Романтика", value: "178" },
{ label: "Рыжий", value: "90" },
{ label: "Сакура", value: "126" },
{ label: "Самолет", value: "93" },
{ label: "Санса старк", value: "277" },
{ label: "Санта", value: "372" },
{ label: "Свадьба", value: "73" },
{ label: "Сверхсила", value: "228" },
{ label: "Сверхъестественное", value: "319" },
{ label: "Свингеры", value: "201" },
{ label: "Свободные отношения", value: "186" },
{ label: "Связывание", value: "137" },
{ label: "Сёдзе", value: "264" },
{ label: "Секс", value: "21" },
{ label: "Секс без проникновения", value: "68" },
{ label: "Секс втроем", value: "43" },
{ label: "Секс игрушки", value: "59" },
{ label: "Секс рабыня", value: "55" },
{ label: "Секс с монстрами", value: "56" },
{ label: "Секс с учителем", value: "184" },
{ label: "Секса будет много", value: "151" },
{ label: "Сексуальное желание", value: "209" },
{ label: "Селфцест", value: "397" },
{ label: "Семья", value: "5" },
{ label: "Сестра", value: "31" },
{ label: "Сестра беременна от брата", value: "355" },
{ label: "Сильный с самого начала", value: "112" },
{ label: "Симбиоз", value: "50" },
{ label: "Сирена", value: "51" },
{ label: "Система", value: "104" },
{ label: "Сиськи", value: "285" },
{ label: "Сквирт", value: "164" },
{ label: "Скрытый секс", value: "287" },
{ label: "Служанка", value: "215" },
{ label: "Соб", value: "281" },
{ label: "Соблазнение", value: "162" },
{ label: "Современность", value: "217" },
{ label: "Соколиный Глаз", value: "176" },
{ label: "Соперничество", value: "216" },
{ label: "Соседи", value: "61" },
{ label: "Соседка", value: "312" },
{ label: "Сперма", value: "229" },
{ label: "Сперма на лицо", value: "199" },
{ label: "Спящие", value: "187" },
{ label: "Старшая сестра", value: "311" },
{ label: "Стеб", value: "111" },
{ label: "Студенты", value: "177" },
{ label: "Суккуб", value: "390" },
{ label: "Супергерои", value: "305" },
{ label: "Суперспособности", value: "300" },
{ label: "Сын", value: "4" },
{ label: "Твинцест", value: "188" },
{ label: "Темное фэнтези", value: "293" },
{ label: "Тентакли", value: "303" },
{ label: "Тетя", value: "113" },
{ label: "Тетя беременна от племянника", value: "376" },
{ label: "Тетя и племянник", value: "377" },
{ label: "Техника", value: "302" },
{ label: "Технологии", value: "301" },
{ label: "Тёща", value: "270" },
{ label: "Тиран", value: "310" },
{ label: "Трагическое прошлое", value: "251" },
{ label: "Трансмиграция", value: "269" },
{ label: "Трансформация", value: "317" },
{ label: "Триллер", value: "344" },
{ label: "Трусики", value: "165" },
{ label: "Убийства", value: "258" },
{ label: "Удача", value: "299" },
{ label: "Ужасы", value: "375" },
{ label: "Улучшение тела", value: "288" },
{ label: "Умная главная героиня", value: "242" },
{ label: "Умные персонажи", value: "257" },
{ label: "Умный главный герой", value: "354" },
{ label: "Университет", value: "323" },
{ label: "Ученик", value: "83" },
{ label: "Ф", value: "149" },
{ label: "Фанатичная любовь", value: "366" },
{ label: "Фанаты", value: "367" },
{ label: "Фантастика", value: "295" },
{ label: "Фанфик", value: "40" },
{ label: "Фелляция", value: "22" },
{ label: "Ферма", value: "191" },
{ label: "Фетиш", value: "60" },
{ label: "Флэш", value: "121" },
{ label: "Футанария", value: "150" },
{ label: "Фэнтези", value: "359" },
{ label: "Хентай", value: "75" },
{ label: "Хината", value: "110" },
{ label: "Холодная главная героиня", value: "263" },
{ label: "Черная вдова", value: "220" },
{ label: "Черная Вдова", value: "175" },
{ label: "Черный юмор", value: "374" },
{ label: "Читерство", value: "45" },
{ label: "Чтение мыслей", value: "227" },
{ label: "Чудовища", value: "378" },
{ label: "Шантаж", value: "129" },
{ label: "Школа", value: "118" },
{ label: "Школьная жизнь", value: "318" },
{ label: "Шлюха", value: "32" },
{ label: "Шоу-бизнес", value: "329" },
{ label: "Шоу-бизнес", value: "172" },
{ label: "Эксгибиционизм", value: "74" },
{ label: "Элементы бдсм", value: "185" },
{ label: "Эмма уотсон", value: "342" },
{ label: "Эмма Уотсон", value: "169" },
{ label: "Эротика", value: "36" },
{ label: "Этти", value: "327" },
{ label: "Юмор", value: "105" },
{ label: "Яндере", value: "155" },
{ label: "Bl", value: "125" },
{ label: "Gl", value: "96" },
{ label: "Harry potter", value: "190" },
{ label: "Harry potter", value: "168" },
{ label: "Hentai", value: "282" },
{ label: "Marvel", value: "194" },
{ label: "Marvel", value: "174" },
{ label: "Milf", value: "167" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "adult",
label: "Возрастные ограничения",
values: [
{ label: "Все", value: "0" },
{ label: "Убрать 18+", value: "1" },
{ label: "Только 18+", value: "2" },
],
inputType: FilterInputs.Picker,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/erolate/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
-215
View File
@@ -1,215 +0,0 @@
const cheerio = require("cheerio");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const pluginId = "jaomix.ru";
const sourceName = "Jaomix";
const baseUrl = "https://jaomix.ru";
async function popularNovels(page, { showLatestNovels, filters }) {
let url = baseUrl + "/?searchrn&sortby=";
url += showLatestNovels ? "upd" : filters?.sort || "count";
if (filters?.type?.length) {
url += filters.type.map((i) => `&lang[]=${i}`).join("");
}
if (filters?.genres?.length) {
url += filters.genres.map((i) => `&genre[]=${i}`).join("");
}
url += `&page=${page}`;
const result = await fetchApi(url);
let body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
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");
novels.push({ name, cover, url });
});
return novels;
}
async function parseNovelAndChapters(novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel = {
url: novelUrl,
};
novel.name = loadedCheerio('div[class="desc-book"] > h1').text().trim();
novel.cover = loadedCheerio('div[class="img-book"] > img').attr("src");
novel.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("продолжается")
? Status.Ongoing
: Status.Completed;
}
});
const chapters = [];
loadedCheerio(".download-chapter div.title").each(function () {
chapters.push({
name: loadedCheerio(this).find("a").attr("title"),
releaseTime: loadedCheerio(this).find("time").text(),
url: loadedCheerio(this).find("a").attr("href"),
});
});
novel.chapters = chapters.reverse();
return novel;
}
async function parseChapter(chapterUrl) {
const result = await fetchApi(chapterUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
loadedCheerio('div[class="adblock-service"]').remove();
const chapterText = loadedCheerio('div[class="entry-content"]').html();
return chapterText;
}
async function searchNovels(searchTerm) {
const url = `${baseUrl}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd`;
const result = await fetchApi(url);
let body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
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");
novels.push({ name, cover, url });
});
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Имя", value: "alphabet" },
{ label: "Просмотры", value: "count" },
{ label: "Дате добавления", value: "new" },
{ label: "Дате обновления", value: "upd" },
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Английский", value: "Английский" },
{ label: "Китайский", value: "Китайский" },
{ label: "Корейский", value: "Корейский" },
{ label: "Японский", value: "Японский" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/jaomix/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { Chapter, Novel, Plugin } from "@typings/plugin";
export const id = "jaomix.ru";
export const name = "Jaomix";
export const site = "https://jaomix.ru";
export const version = "1.0.0";
export const icon = "src/ru/jaomix/icon.png";
const baseUrl = site;
export const popularNovels: Plugin.popularNovels = async function (
page,
{ showLatestNovels, filters }
) {
let url = baseUrl + "/?searchrn&sortby=";
url += showLatestNovels ? "upd" : filters?.sort || "count";
if (filters) {
if (Array.isArray(filters.type) && filters.type.length) {
url += filters.type.map((i) => `&lang[]=${i}`).join("");
}
if (Array.isArray(filters.genres) && filters.genres.length) {
url += filters.genres.map((i) => `&genre[]=${i}`).join("");
}
}
url += `&page=${page}`;
const result = await fetchApi(url);
let body = await result.text();
const loadedCheerio = parseHTML(body);
let novels: Novel.Item[] = [];
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;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
let novel: Novel.instance = {
url: novelUrl,
};
novel.name = loadedCheerio('div[class="desc-book"] > h1').text().trim();
novel.cover = loadedCheerio('div[class="img-book"] > img').attr("src");
novel.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: Chapter.Item[] = [];
loadedCheerio(".download-chapter div.title").each(function () {
chapters.push({
name: loadedCheerio(this).find("a").attr("title")!,
releaseTime: loadedCheerio(this).find("time").text(),
url: loadedCheerio(this).find("a").attr("href")!,
});
});
novel.chapters = chapters.reverse();
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi(chapterUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio('div[class="adblock-service"]').remove();
const chapterText = loadedCheerio('div[class="entry-content"]').html();
return chapterText;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const url = `${baseUrl}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd`;
const result = await fetchApi(url);
let body = await result.text();
const loadedCheerio = parseHTML(body);
let novels: Novel.Item[] = [];
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;
};
export const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Имя", value: "alphabet" },
{ label: "Просмотры", value: "count" },
{ label: "Дате добавления", value: "new" },
{ label: "Дате обновления", value: "upd" },
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Английский", value: "Английский" },
{ label: "Китайский", value: "Китайский" },
{ label: "Корейский", value: "Корейский" },
{ label: "Японский", value: "Японский" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
];
export const fetchImage: Plugin.fetchImage = fetchFile;
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "japit.novel";
export const name = "Japit Comics";
export const icon = "";
export const version = "1.0.0";
export const site = "https://japit.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Академия","value":5},{"label":"Андроиды","value":37},{"label":"Беззаботный герой","value":115},{"label":"Боги","value":318},{"label":"Богини","value":316},{"label":"Вампиры","value":742},{"label":"Везучий главный герой","value":411},{"label":"Влияние прошлого","value":517},{"label":"Внезапное обогащение","value":691},{"label":"Военные хроники","value":749},{"label":"Война","value":750},{"label":"Воспоминания прошлого","value":283},{"label":"Герои","value":341},{"label":"Героиня красавица","value":81},{"label":"Герой влюбляется первым","value":561},{"label":"Герой красавец","value":329},{"label":"Герой-бесстыдник","value":632},{"label":"Герой-извращенец","value":521},{"label":"Герой-трудяга","value":330},{"label":"R-15","value":570}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "kg.novel";
export const name = "Карманная галактика";
export const icon = "";
export const version = "1.0.0";
export const site = "https://kg.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Алхимия","value":27},{"label":"Андроиды","value":37},{"label":"Анти-рост персонажа","value":25},{"label":"Антигерой","value":43},{"label":"Апатичный главный герой","value":46},{"label":"Аристократия","value":51},{"label":"Асоциальный главный герой","value":42},{"label":"Ассасины","value":61},{"label":"Беззаботный герой","value":115},{"label":"Безработные","value":377},{"label":"Бессмертные","value":359},{"label":"Библиотека","value":393},{"label":"Боги","value":318},{"label":"Боевая академия","value":75},{"label":"Бывший герой","value":290},{"label":"Ведьмы","value":756},{"label":"Везучий главный герой","value":411},{"label":"Верные подданные","value":410},{"label":"Взросление персонажа","value":24},{"label":"R-15","value":570}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "kod.novel";
export const name = "K.O.D.";
export const icon = "";
export const version = "1.0.0";
export const site = "https://kod.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Актёрское искусство","value":7},{"label":"Забота о детях","value":127},{"label":"Управление бизнесом","value":109},{"label":"Шоу-бизнес","value":642}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "nightnovel";
export const name = "Nightnovel";
export const icon = "";
export const version = "1.0.0";
export const site = "https://nightnovel.online";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Альтернативная реальность","value":30},{"label":"Амнезия","value":31},{"label":"Анал","value":33},{"label":"Ангелы","value":38},{"label":"Аристократия","value":51},{"label":"Артефакты","value":58},{"label":"Ассасины","value":61},{"label":"Банды","value":302},{"label":"БДСМ","value":77},{"label":"Беременность","value":551},{"label":"Боги","value":318},{"label":"Богини","value":316},{"label":"Боевая академия","value":75},{"label":"Божественная сила","value":317},{"label":"Влияние прошлого","value":517},{"label":"Враги становятся союзниками","value":238},{"label":"Вынужденные условия проживания","value":287},{"label":"Герои","value":341},{"label":"Героиня красавица","value":81},{"label":"R-18","value":571}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+867
View File
@@ -0,0 +1,867 @@
import cheerio from "cheerio";
import dayjs from "dayjs";
import { fetchApi } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { Chapter, Novel, Plugin } from "@typings/plugin";
export const id = "TL";
export const name = "NovelTL";
export const site = "https://novel.tl";
export const version = "1.0.0";
export const icon = "src/ru/noveltl/icon.png";
const baseUrl = site;
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: baseUrl,
body: JSON.stringify({
operationName: "Projects",
query: "query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}",
variables: {
filter: {
tags: filters?.tags || [],
genres: filters?.genres || [],
},
hostname: "novel.tl",
limit: 40,
page,
},
}),
});
const json = await result.json();
let novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel) =>
novels.push({
name: novel.title,
url: "https://" + novel.fullUrl,
cover: novel?.covers?.[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
})
);
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: baseUrl,
body: JSON.stringify({
operationName: "Book",
query: 'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus fullUrl covers{url}persons(langs:["ru","en","*"],roles:["author","illustrator"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{shortName chapters{title publishDate fullUrl published}}}}}}}',
variables: {
url: novelUrl,
},
}),
});
const json = await result.json();
let novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project?.covers?.[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: json.data.project?.annotation?.text,
status:
json.data.project.translationStatus === "active"
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item) => item?.nameRu || item?.nameEng)
.filter((item) => item);
if (genres?.length) {
novel.genres = genres.join(", ");
}
json.data.project.persons.forEach((person) => {
if (person.role == "author" && person.name.firstName) {
novel.author =
person.name.firstName + " " + (person.name?.lastName || "");
} else if (person.role == "illustrator" && person.name.firstName) {
novel.artist =
person.name.firstName + " " + (person.name?.lastName || "");
}
});
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work) =>
work.volumes.content.forEach((volume) =>
volume.chapters.forEach(
(chapter) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + " " + chapter.title,
url: "https://" + chapter.fullUrl,
releaseTime: dayjs(chapter.publishDate).format(
"LLL"
),
})
)
)
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: baseUrl,
body: JSON.stringify({
query: "query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}",
variables: {
url: chapterUrl,
},
}),
});
const json = await result.json();
const loadedCheerio = cheerio.load(json.data.chapter.text.text);
loadedCheerio("p > a[href]").each(function () {
let url = baseUrl + loadedCheerio(this).attr("href");
if (!url.startsWith("http")) {
url = baseUrl + url;
}
loadedCheerio(this).find("picture").remove();
loadedCheerio(this).removeAttr("href");
loadedCheerio(`<img src="${url}">`).appendTo(this);
});
const chapterText = loadedCheerio.html();
return chapterText;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: this.baseUrl,
body: JSON.stringify({
operationName: "Projects",
query: "query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}",
variables: {
filter: {
query: searchTerm,
},
hostname: "novel.tl",
limit: 40,
page: 1,
},
}),
});
const json = await result.json();
let novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel) =>
novels.push({
name: novel.title,
url: "https://" + novel.fullUrl,
cover: novel?.covers?.[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
})
);
return novels;
};
export const filters = [
{
key: "tags",
label: "Тэги",
values: [
{ label: "Автоматы", value: 64 },
{ label: "Агрессивные персонажи", value: 26 },
{ label: "Ад", value: 338 },
{ label: "Адаптация манги", value: 8 },
{ label: "Академия", value: 5 },
{ label: "Актёрское искусство", value: 7 },
{ label: "Актеры озвучки", value: 746 },
{ label: "Алхимия", value: 27 },
{ label: "Альтернативная реальность", value: 30 },
{ label: "Амнезия", value: 31 },
{ label: "Анабиоз", value: 170 },
{ label: "Анал", value: 33 },
{ label: "Ангелы", value: 38 },
{ label: "Андроиды", value: 37 },
{ label: "Анти-рост персонажа", value: 25 },
{ label: "Антигерой", value: 43 },
{ label: "Антикварный магазин", value: 44 },
{ label: "Антимагия", value: 41 },
{ label: "Антиутопия", value: 225 },
{ label: "Апатичный главный герой", value: 46 },
{ label: "Апокалипсис", value: 47 },
{ label: "Аристократия", value: 51 },
{ label: "Армия", value: 53 },
{ label: "Артефакты", value: 58 },
{ label: "Асоциальный главный герой", value: 42 },
{ label: "Ассасины", value: 61 },
{ label: "Атмосфера Европы", value: 245 },
{ label: "Банды", value: 302 },
{ label: "БДСМ", value: 77 },
{ label: "Бедный главный герой", value: 541 },
{ label: "Бездушный главный герой", value: 143 },
{ label: "Беззаботный герой", value: 115 },
{ label: "Безответная любовь", value: 213 },
{ label: "Безответная любовь", value: 740 },
{ label: "Безработные", value: 377 },
{ label: "Беременность", value: 551 },
{ label: "Бескорыстная любовь", value: 733 },
{ label: "Бессмертные", value: 359 },
{ label: "Бесстрашный главный герой", value: 274 },
{ label: "Библиотека", value: 393 },
{ label: "Биография", value: 86 },
{ label: "Близнецы", value: 729 },
{ label: "Богачи", value: 753 },
{ label: "Боги", value: 318 },
{ label: "Богини", value: 316 },
{ label: "Богоподобный протагонист", value: 314 },
{ label: "Боевая академия", value: 75 },
{ label: "Боевая парочка", value: 547 },
{ label: "Божественная защита", value: 208 },
{ label: "Божественная сила", value: 317 },
{ label: "Болеющие персонажи", value: 648 },
{ label: "Борьба за власть", value: 548 },
{ label: "Брак по расчету", value: 55 },
{ label: "Братство", value: 106 },
{ label: "Братья или сёстры", value: 646 },
{ label: "Брошенные дети", value: 1 },
{ label: "Бывший герой", value: 290 },
{ label: "Вампиры", value: 742 },
{ label: "Ваншот", value: 496 },
{ label: "Вдали от родителей", value: 3 },
{ label: "Ведьмы", value: 756 },
{ label: "Везучий главный герой", value: 411 },
{ label: "Верные подданные", value: 410 },
{ label: "Вероломные герои/плетение интриг", value: 424 },
{ label: "Взросление персонажа", value: 24 },
{ label: "Видит то, чего остальной люд не в силах", value: 617 },
{ label: "Виртуальная реальность", value: 744 },
{ label: "Владелец магазина", value: 677 },
{ label: "Владелец уникального оружия", value: 736 },
{ label: "Властные персонажи", value: 545 },
{ label: "Влияние прошлого", value: 517 },
{ label: "Внезапное обогащение", value: 691 },
{ label: "Внезапное обретение силы", value: 690 },
{ label: "Военные хроники", value: 749 },
{ label: "Возвращение в родной мир", value: 587 },
{ label: "Война", value: 750 },
{ label: "Вокалоид", value: 745 },
{ label: "Волшебные твари", value: 413 },
{ label: "Воскрешение", value: 586 },
{ label: "Воспитанный главный герой", value: 537 },
{ label: "Воспоминания прошлого", value: 283 },
{ label: "Враги становятся союзниками", value: 238 },
{ label: "Врата в иной мир", value: 303 },
{ label: "Временной парадокс", value: 711 },
{ label: "Вторжение на Землю", value: 228 },
{ label: "Второй шанс", value: 608 },
{ label: "Выживание", value: 695 },
{ label: "Вынужденные условия проживания", value: 287 },
{ label: "Высокомерный герой", value: 56 },
{ label: "Гадание", value: 207 },
{ label: "Галлюцинации", value: 189 },
{ label: "Гарем рабов", value: 656 },
{ label: "Геймеры", value: 301 },
{ label: "Генетические модификации", value: 306 },
{ label: "Герои", value: 341 },
{ label: "Герои непонятного пола", value: 36 },
{ label: "Героиня девушка-сорванец", value: 715 },
{ label: "Героиня красавица", value: 81 },
{ label: "Герой влюбляется первым", value: 561 },
{ label: "Герой использует щит", value: 636 },
{ label: "Герой красавец", value: 329 },
{ label: "Герой со множеством тел", value: 564 },
{ label: "Герой-бесстыдник", value: 632 },
{ label: "Герой-извращенец", value: 521 },
{ label: "Герой-трудяга", value: 330 },
{ label: "Герой-яндере", value: 421 },
{ label: "Гетерохромия", value: 342 },
{ label: "Гильдии", value: 324 },
{ label: "Гиперопека", value: 509 },
{ label: "Главная героиня — женщина", value: 277 },
{ label: "Главный герой - гений", value: 308 },
{ label: "Главный герой - Моб", value: 446 },
{ label: "Главный герой — знаменитость", value: 263 },
{ label: "Главный герой — мужчина", value: 419 },
{ label: "Главный герой в поисках гарема", value: 331 },
{ label: "Главный герой заранее готовится", value: 557 },
{ label: "Главный герой не человек", value: 488 },
{ label: "Главный герой приёмный", value: 20 },
{ label: "Главный герой сразу силён", value: 563 },
{ label: "Гладиаторы", value: 310 },
{ label: "Глухой к любви герой", value: 194 },
{ label: "Гоблины", value: 313 },
{ label: "Големы", value: 319 },
{ label: "Гомункул", value: 347 },
{ label: "Горничные", value: 418 },
{ label: "Государственные интриги", value: 212 },
{ label: "Гринд", value: 322 },
{ label: "Давняя травма", value: 518 },
{ label: "Двойник", value: 97 },
{ label: "Дворецкие", value: 111 },
{ label: "Дворфы", value: 224 },
{ label: "Дворяне", value: 487 },
{ label: "Двоюродные братья или сёстры", value: 162 },
{ label: "Девочки-волшебницы", value: 415 },
{ label: "Демоны", value: 193 },
{ label: "Депрессия", value: 196 },
{ label: "Детектив", value: 198 },
{ label: "Детективное расследование", value: 473 },
{ label: "Детское насилие", value: 125 },
{ label: "Детское обещание", value: 130 },
{ label: "Дискриминация", value: 203 },
{ label: "Длительный разрыв отношений", value: 400 },
{ label: "Домашняя жизнь", value: 45 },
{ label: "Драконы", value: 218 },
{ label: "Древний Китай", value: 34 },
{ label: "Дружба", value: 293 },
{ label: "Друзья детства", value: 128 },
{ label: "Друзья становятся врагами", value: 292 },
{ label: "Духи", value: 673 },
{ label: "Духовная сила", value: 665 },
{ label: "Духовный наставник", value: 671 },
{ label: "Душещипательная история", value: 335 },
{ label: "Души", value: 666 },
{ label: "Дьявольский путь культивации", value: 192 },
{ label: "Ёкай", value: 763 },
{ label: "Есть аниме-адаптация", value: 10 },
{ label: "Есть видеоигра", value: 13 },
{ label: "Есть дорама-адаптация", value: 11 },
{ label: "Есть манга-адаптация", value: 14 },
{ label: "Есть манхва-адаптация", value: 16 },
{ label: "Есть маньхуа-адаптация", value: 15 },
{ label: "Есть фильм", value: 17 },
{ label: "Есть CD дорама-адаптация", value: 12 },
{ label: "Жёсткий главный герой", value: 597 },
{ label: "Жестокие персонажи", value: 4 },
{ label: "Жёстокие персонажи", value: 169 },
{ label: "Животное-спутник", value: 78 },
{ label: "Жизнь в одиночку", value: 396 },
{ label: "Жрецы", value: 555 },
{ label: "Жрицы", value: 554 },
{ label: "Забота о детях", value: 127 },
{ label: "Заботливый главный герой", value: 116 },
{ label: "Загадочное заболевание", value: 471 },
{ label: "Загадочное прошлое родителей", value: 470 },
{ label: "Заговоры", value: 154 },
{ label: "Закалка тела", value: 96 },
{ label: "Закон джунглей", value: 682 },
{ label: "Закулисная борба", value: 603 },
{ label: "Замкнутый главный герой", value: 371 },
{ label: "Запечатанная сила", value: 607 },
{ label: "Затворник", value: 346 },
{ label: "Звери", value: 80 },
{ label: "Зверолюди", value: 39 },
{ label: "Звероподобные", value: 79 },
{ label: "Земледелие", value: 268 },
{ label: "Злобные благородные девы", value: 743 },
{ label: "Зловещие организации", value: 247 },
{ label: "Злой главный герой", value: 248 },
{ label: "Злые боги", value: 246 },
{ label: "Злые религии", value: 249 },
{ label: "Знаменитости", value: 118 },
{ label: "Знания из прошлой жизни", value: 553 },
{ label: "Зомби", value: 767 },
{ label: "Игра на выживание", value: 696 },
{ label: "Игровая рейтинговая система", value: 300 },
{ label: "Из грязи в князи", value: 542 },
{ label: "Известные родители", value: 262 },
{ label: "Изменение внешнего вида", value: 48 },
{ label: "Изнасилование", value: 574 },
{ label: "Инвалидность", value: 202 },
{ label: "Индустриализация", value: 364 },
{ label: "Инженер", value: 241 },
{ label: "Инцест", value: 361 },
{ label: "Искажённая личность", value: 730 },
{ label: "Искусственный интеллект", value: 59 },
{ label: "Исполнение желаний", value: 755 },
{ label: "Каннибализм", value: 113 },
{ label: "Карточные игры", value: 114 },
{ label: "Кладбищенский смотритель", value: 321 },
{ label: "Классика", value: 134 },
{ label: "Книги", value: 99 },
{ label: "Книги навыков", value: 654 },
{ label: "Книжный червь", value: 100 },
{ label: "Коллеги", value: 140 },
{ label: "Колледж/Университет", value: 145 },
{ label: "Командная работа", value: 701 },
{ label: "Комедийный подтекст", value: 147 },
{ label: "Комплекс брата", value: 105 },
{ label: "Комплекс неполноценности", value: 365 },
{ label: "Контракты", value: 155 },
{ label: "Контроль разума", value: 441 },
{ label: "Конфликт в семье", value: 261 },
{ label: "Конфликт верности", value: 153 },
{ label: "Копейщик", value: 668 },
{ label: "Королевская особа", value: 596 },
{ label: "Королевства", value: 382 },
{ label: "Короткий рассказ", value: 638 },
{ label: "Коррупция", value: 157 },
{ label: "Космические Войны", value: 158 },
{ label: "Космос", value: 507 },
{ label: "Косплей", value: 159 },
{ label: "Кража способностей", value: 2 },
{ label: "Крафтинг", value: 164 },
{ label: "Кризис личности", value: 357 },
{ label: "Кровь", value: 320 },
{ label: "Кружки, клубы", value: 138 },
{ label: "Кудэрэ", value: 384 },
{ label: "Кузнец", value: 90 },
{ label: "Кукловоды", value: 567 },
{ label: "Куклы/Марионетки", value: 211 },
{ label: "Кулинария", value: 156 },
{ label: "Культивация", value: 171 },
{ label: "Куннилингус", value: 172 },
{ label: "Легенды", value: 391 },
{ label: "Легкая жизнь", value: 229 },
{ label: "Лекари", value: 334 },
{ label: "Ленивый главный герой", value: 389 },
{ label: "Лидерство", value: 390 },
{ label: "Любит персонажа младше себя", value: 765 },
{ label: "Любит персонажа старше себя", value: 494 },
{ label: "Любовное соперничество", value: 406 },
{ label: "Любовные треугольники", value: 407 },
{ label: "Любовь детства", value: 129 },
{ label: "Любовь с первого взгляда", value: 404 },
{ label: "Любящие родители", value: 215 },
{ label: "Маги", value: 757 },
{ label: "Магические технологии", value: 417 },
{ label: "Магия", value: 412 },
{ label: "Магия призыва", value: 694 },
{ label: "Маленькие девочки", value: 397 },
{ label: "Мангака", value: 423 },
{ label: "Маниакальная любовь", value: 492 },
{ label: "Марти Сью", value: 508 },
{ label: "Мастер подземелий", value: 222 },
{ label: "Мастурбация", value: 432 },
{ label: "Матриархат", value: 433 },
{ label: "Межпространственные путешествия", value: 370 },
{ label: "Менеджмент", value: 422 },
{ label: "Месть", value: 588 },
{ label: "Мечи и магия", value: 697 },
{ label: "Мечники", value: 698 },
{ label: "Миленькие дети", value: 176 },
{ label: "Милитаризм", value: 439 },
{ label: "Миловидная история", value: 178 },
{ label: "Милый главный герой", value: 177 },
{ label: "Минет", value: 275 },
{ label: "Мировое древо", value: 760 },
{ label: "Миролюбивый главный герой", value: 510 },
{ label: "Мифология", value: 475 },
{ label: "Младшие сёстры", value: 766 },
{ label: "ММОРПГ", value: 445 },
{ label: "Множество временных линий", value: 463 },
{ label: "Модели", value: 447 },
{ label: "Молод не по годам", value: 49 },
{ label: "Монстры", value: 454 },
{ label: "Мрачность", value: 183 },
{ label: "Музыка", value: 466 },
{ label: "Музыкальные группы", value: 68 },
{ label: "Мутации", value: 468 },
{ label: "На волосок от смерти", value: 479 },
{ label: "На все руки мастер", value: 374 },
{ label: "Навыки имеют стоимость", value: 150 },
{ label: "Нагота", value: 490 },
{ label: "Наездники на драконах", value: 216 },
{ label: "Наёмники", value: 437 },
{ label: "Наивный главный герой", value: 476 },
{ label: "Наследие", value: 366 },
{ label: "Настоящая любовь", value: 200 },
{ label: "Настоящий гарем", value: 540 },
{ label: "Небесное испытание", value: 337 },
{ label: "Невезучий главный герой", value: 738 },
{ label: "Недоверчивый протагонист", value: 206 },
{ label: "Недооцененный главный герой", value: 734 },
{ label: "Недопонимание", value: 444 },
{ label: "Некромант", value: 480 },
{ label: "Нелинейное повествование", value: 489 },
{ label: "Неловкий главный герой", value: 67 },
{ label: "Нелюбимый протагонист", value: 333 },
{ label: "Немой персонаж", value: 469 },
{ label: "Ненадежный рассказчик", value: 739 },
{ label: "Непримечательный герой", value: 65 },
{ label: "Нерешительный протагонист", value: 363 },
{ label: "Несгибаемый главный герой", value: 676 },
{ label: "Несколько главных героев", value: 460 },
{ label: "Несколько миров", value: 461 },
{ label: "Несколько перерожденцев", value: 462 },
{ label: "Несколько попаданцев", value: 464 },
{ label: "Несравненная скорость культивации", value: 269 },
{ label: "Нехватка/отсутствие здравого смысла", value: 385 },
{ label: "Нечестный протагонист", value: 205 },
{ label: "НИИТ", value: 481 },
{ label: "Ниндзя", value: 486 },
{ label: "Обмен телами", value: 95 },
{ label: "Оборотни", value: 754 },
{ label: "Объект любви влюбился первым", value: 405 },
{ label: "Объект любви популярен", value: 543 },
{ label: "Огнестрельное оружие", value: 280 },
{ label: "Ограничение на время жизни", value: 394 },
{ label: "Одержимость", value: 544 },
{ label: "Один родитель", value: 651 },
{ label: "Одинокий главный герой", value: 399 },
{ label: "Одиночество", value: 398 },
{ label: "Одно тело на двоих", value: 634 },
{ label: "Око силы", value: 253 },
{ label: "Оммёдзи", value: 498 },
{ label: "Организованная преступность", value: 500 },
{ label: "Оргия", value: 501 },
{ label: "Орки", value: 499 },
{ label: "Основано на аниме", value: 74 },
{ label: "Основано на видеоигре", value: 72 },
{ label: "Основано на визуальной новелле", value: 73 },
{ label: "Основано на песне", value: 70 },
{ label: "Основано на фильме", value: 69 },
{ label: "Осторожный главный герой", value: 117 },
{ label: "От ненависти до любви один шаг", value: 239 },
{ label: "От сильного с сильнейшему", value: 684 },
{ label: "От слабого к сильному", value: 752 },
{ label: "Отаку", value: 503 },
{ label: "Отзывчивая любовь", value: 380 },
{ label: "Отзывчивый главный герой", value: 339 },
{ label: "Отношение учитель-ученик", value: 430 },
{ label: "Отношения бог-человек", value: 315 },
{ label: "Отношения между учителем и учеником", value: 687 },
{ label: "Отношения начальник-подчинённый", value: 101 },
{ label: "Отношения против воли", value: 286 },
{ label: "Отношения с нечеловеком", value: 353 },
{ label: "Отношения сэмпай-кохай", value: 621 },
{ label: "Отношения хозяин-слуга", value: 431 },
{ label: "Отомэ-игры", value: 504 },
{ label: "ОТП", value: 562 },
{ label: "Отшельники", value: 505 },
{ label: "Офисный работник", value: 600 },
{ label: "Официанты", value: 748 },
{ label: "Охотники", value: 355 },
{ label: "Падшие ангелы", value: 255 },
{ label: "Падшие дворяне", value: 256 },
{ label: "Пайзури", value: 511 },
{ label: "Паразиты", value: 513 },
{ label: "Параллельные миры", value: 512 },
{ label: "Парк развлечений", value: 32 },
{ label: "Пародия", value: 515 },
{ label: "Перемещение между мирами", value: 758 },
{ label: "Переодевание", value: 167 },
{ label: "Переплавка пилюль", value: 529 },
{ label: "Перерождение в девушку", value: 420 },
{ label: "Перерождение в другом мире", value: 580 },
{ label: "Перерождение в другом мире", value: 720 },
{ label: "Перерождение в игровом мире", value: 579 },
{ label: "Перерождение в монстра", value: 577 },
{ label: "Персонажи с мазохистскими наклонностями", value: 429 },
{ label: "Персонажи с садистскими наклонностями", value: 598 },
{ label: "Петля времени", value: 709 },
{ label: "Петтинг", value: 328 },
{ label: "Писатели", value: 761 },
{ label: "Питомцы", value: 522 },
{ label: "Племенное общество", value: 726 },
{ label: "Повелитель демонов", value: 191 },
{ label: "Повествование от нескольких лиц", value: 459 },
{ label: "Поглощение навыков", value: 653 },
{ label: "Подземелья", value: 223 },
{ label: "Поздно начинающаяся романтическая линия", value: 387 },
{ label: "Политика", value: 538 },
{ label: "Полулюди", value: 190 },
{ label: "Помолвка", value: 240 },
{ label: "Попаданец в другой мир", value: 723 },
{ label: "Попаданец в игровой мир", value: 722 },
{ label: "Постапокалипсис", value: 546 },
{ label: "Постижение Дао", value: 181 },
{ label: "Потеря девственности", value: 282 },
{ label: "Потустороннее пространство", value: 416 },
{ label: "Похищения", value: 379 },
{ label: "Прагматичный главный герой", value: 549 },
{ label: "Предательство", value: 83 },
{ label: "Предвидение", value: 550 },
{ label: "Преследователи", value: 674 },
{ label: "Преступление", value: 165 },
{ label: "Преступность", value: 166 },
{ label: "Приёмные дети", value: 19 },
{ label: "Призванный герой", value: 693 },
{ label: "Призраки", value: 309 },
{ label: "Приключения", value: 22 },
{ label: "Принуждение", value: 108 },
{ label: "Приручатель", value: 453 },
{ label: "Прислуга", value: 625 },
{ label: "Приставучий любовник", value: 136 },
{ label: "Притворная любовь", value: 552 },
{ label: "Пришельцы", value: 28 },
{ label: "Программист", value: 558 },
{ label: "Произведение - обладатель наград", value: 66 },
{ label: "Проклятия", value: 175 },
{ label: "Промывка мозгов", value: 102 },
{ label: "Пропуск времени", value: 712 },
{ label: "Пророчества", value: 559 },
{ label: "Протагонист в очках", value: 312 },
{ label: "Протагонист раб", value: 657 },
{ label: "Психокинез", value: 565 },
{ label: "Психопаты", value: 566 },
{ label: "Путешествия", value: 759 },
{ label: "Путешествия во времени", value: 713 },
{ label: "Пытки", value: 716 },
{ label: "Рабы", value: 658 },
{ label: "Развитие отношений", value: 160 },
{ label: "Развитие персонажа", value: 119 },
{ label: "Раздвоение личности", value: 458 },
{ label: "Размеренная романтика", value: 661 },
{ label: "Разница в статусе", value: 201 },
{ label: "Разорванная помолвка", value: 104 },
{ label: "Разумные предметы", value: 622 },
{ label: "Рай", value: 336 },
{ label: "Раняя романтика", value: 227 },
{ label: "Расизм", value: 573 },
{ label: "Расследования", value: 372 },
{ label: "Ребенок — главный герой", value: 126 },
{ label: "Реверс-гарем", value: 589 },
{ label: "Реверс-изнасилование", value: 590 },
{ label: "Резкая смена характера", value: 520 },
{ label: "Реинкарнация", value: 581 },
{ label: "Религии", value: 582 },
{ label: "Ресторан", value: 585 },
{ label: "Решительный главный герой", value: 199 },
{ label: "Робкий протагонист", value: 714 },
{ label: "Родительсткий комплекс", value: 514 },
{ label: "Родные не связанные кровью", value: 647 },
{ label: "Родословные", value: 94 },
{ label: "РПГ-система", value: 392 },
{ label: "Рыцари", value: 383 },
{ label: "Самовлюбленный главный герой", value: 477 },
{ label: "Самоубийства", value: 692 },
{ label: "Самураи", value: 601 },
{ label: "Сборник коротких историй", value: 144 },
{ label: "Свадьба", value: 426 },
{ label: "Связанные сюжетные линии", value: 369 },
{ label: "Святые", value: 599 },
{ label: "Секреты", value: 614 },
{ label: "Секс втроём", value: 707 },
{ label: "Секс-рабыня", value: 629 },
{ label: "Семейная любовь", value: 257 },
{ label: "Семь добродетелей", value: 627 },
{ label: "Семь смертных грехов", value: 626 },
{ label: "Серийные убийцы", value: 624 },
{ label: "Сикигами", value: 637 },
{ label: "Сильная любовь от старших", value: 214 },
{ label: "Синдром восьмиклассника", value: 132 },
{ label: "Синдром сестры", value: 652 },
{ label: "Сироты", value: 502 },
{ label: "Системный администратор", value: 699 },
{ label: "Скромный главный герой", value: 409 },
{ label: "Скрытые романтические отношения", value: 23 },
{ label: "Скрытые способности", value: 343 },
{ label: "Скряга", value: 450 },
{ label: "Скульпторы", value: 606 },
{ label: "Слабая романтика", value: 688 },
{ label: "Слабый главный герой", value: 751 },
{ label: "Сложные семейные отношения", value: 149 },
{ label: "Смена расы", value: 572 },
{ label: "Смертельное заболевание", value: 704 },
{ label: "Смерть", value: 185 },
{ label: "Смерть любимых", value: 186 },
{ label: "Современность", value: 448 },
{ label: "Современные знания в слаборазвитых мирах", value: 449 },
{ label: "Сожительство", value: 141 },
{ label: "Создание артефактов", value: 57 },
{ label: "Создание клана", value: 133 },
{ label: "Создание королевства", value: 381 },
{ label: "Создание навыков", value: 655 },
{ label: "Сокрытие истинных способностей", value: 344 },
{ label: "Сокрытие личности", value: 345 },
{ label: "Солдаты", value: 664 },
{ label: "Сон", value: 659 },
{ label: "Соседи по комнате", value: 595 },
{ label: "Социальные изгои", value: 663 },
{ label: "Спасение мира", value: 602 },
{ label: "Спорящая пара", value: 84 },
{ label: "Способность перевоплощения", value: 719 },
{ label: "Средневековье", value: 436 },
{ label: "Стеснительные персонажи", value: 643 },
{ label: "Стокгольмский синдром", value: 575 },
{ label: "Стокгольмский синдром", value: 675 },
{ label: "Стратег", value: 681 },
{ label: "Стратегические битвы", value: 680 },
{ label: "Стрелки", value: 325 },
{ label: "Стрельба из лука", value: 50 },
{ label: "Студенческий совет", value: 686 },
{ label: "Судьба", value: 197 },
{ label: "Суккубы", value: 689 },
{ label: "Суперспособности", value: 669 },
{ label: "Суровая тренировка", value: 332 },
{ label: "Сцены насилия", value: 195 },
{ label: "Таинственное прошлое", value: 472 },
{ label: "Тайная личность", value: 610 },
{ label: "Тайные организации", value: 611 },
{ label: "Телохранители", value: 98 },
{ label: "Тентакли", value: 703 },
{ label: "Террористы", value: 705 },
{ label: "Технологический разрыв", value: 702 },
{ label: "Тихие герои", value: 568 },
{ label: "Толстый протагонист", value: 271 },
{ label: "Торговцы", value: 438 },
{ label: "Травник", value: 340 },
{ label: "Трагичное прошлое", value: 718 },
{ label: "Трап", value: 725 },
{ label: "Триллер", value: 708 },
{ label: "Турнир", value: 76 },
{ label: "Тюрьма", value: 556 },
{ label: "Убийства", value: 465 },
{ label: "Уверенный в себе главный герой", value: 151 },
{ label: "Умная пара", value: 662 },
{ label: "Умный главный герой", value: 135 },
{ label: "Уникальная техника культивации", value: 735 },
{ label: "Уникальные оружия", value: 737 },
{ label: "Управление бизнесом", value: 109 },
{ label: "Управление временем", value: 710 },
{ label: "Управление пространством", value: 667 },
{ label: "Ускоренный рост", value: 6 },
{ label: "Учёные", value: 605 },
{ label: "Учителя", value: 700 },
{ label: "Фамильяры", value: 258 },
{ label: "Фанатизм", value: 264 },
{ label: "Фармацевт", value: 523 },
{ label: "Феи", value: 254 },
{ label: "Фениксы", value: 526 },
{ label: "Фетиш на грудь", value: 103 },
{ label: "Фетиш на очки", value: 311 },
{ label: "Философия", value: 524 },
{ label: "Фобии/Страхи", value: 525 },
{ label: "Фольклор", value: 285 },
{ label: "Формирование армии", value: 54 },
{ label: "Футанари", value: 295 },
{ label: "Футуристический мир", value: 296 },
{ label: "Фэнтези", value: 267 },
{ label: "Фэнтезийные существа", value: 266 },
{ label: "Хакеры", value: 326 },
{ label: "Харизматичный протагонист", value: 120 },
{ label: "Хитрый главный герой", value: 173 },
{ label: "Хладнокровный главный герой", value: 112 },
{ label: "Художники", value: 60 },
{ label: "Хулиганы", value: 188 },
{ label: "Цундере", value: 728 },
{ label: "Чат", value: 122 },
{ label: "Человек-оружие", value: 352 },
{ label: "Человекоподобный главный герой", value: 354 },
{ label: "Честный главный герой", value: 348 },
{ label: "Четкие любовные убеждения", value: 683 },
{ label: "Читы", value: 123 },
{ label: "Чудаковатые герои", value: 569 },
{ label: "Чужие воспоминания", value: 721 },
{ label: "Шантаж", value: 89 },
{ label: "Шеф-повар", value: 124 },
{ label: "Школа для девочек", value: 29 },
{ label: "Шоу-бизнес", value: 642 },
{ label: "Шпионы", value: 670 },
{ label: "Эволюция", value: 250 },
{ label: "Эгоистичный главный герой", value: 618 },
{ label: "Экзорцизм", value: 252 },
{ label: "Экономика", value: 230 },
{ label: "Эксгибиционизм", value: 251 },
{ label: "Эксперименты на людях", value: 351 },
{ label: "Элементальная магия", value: 234 },
{ label: "Элементы игрового мира", value: 299 },
{ label: "Элементы романтики", value: 594 },
{ label: "Элементы юри", value: 640 },
{ label: "Элементы яоя", value: 641 },
{ label: "Эльфы", value: 235 },
{ label: "Эпизодичность", value: 243 },
{ label: "Язвительные персонажи", value: 635 },
{ label: "Языковой барьер", value: 386 },
{ label: "Яндере", value: 762 },
{ label: "Яойщица", value: 294 },
{ label: "Японские силы самообороны", value: 378 },
{ label: "R-15", value: 570 },
{ label: "R-18", value: 571 },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ label: "16+", value: 1 },
{ label: "18+", value: 2 },
{ label: "Боевик", value: 3 },
{ label: "Боевые искусства", value: 4 },
{ label: "Вампиры", value: 47 },
{ label: "Военное", value: 44 },
{ label: "Гарем", value: 5 },
{ label: "Демоны", value: 48 },
{ label: "Детектив", value: 6 },
{ label: "Дзёсей", value: 50 },
{ label: "Драма", value: 7 },
{ label: "Игры", value: 45 },
{ label: "Историческое", value: 8 },
{ label: "Киберпанк", value: 9 },
{ label: "Комедия", value: 10 },
{ label: "Космос", value: 46 },
{ label: "Магия", value: 42 },
{ label: "Махо-сёдзё", value: 12 },
{ label: "Меха", value: 13 },
{ label: "Мистика", value: 14 },
{ label: "Музыка", value: 49 },
{ label: "Научная фантастика", value: 15 },
{ label: "Пародия", value: 16 },
{ label: "Повседневность", value: 17 },
{ label: "Приключения", value: 18 },
{ label: "Психологическое", value: 19 },
{ label: "Романтика", value: 20 },
{ label: "Сверхъестественное", value: 21 },
{ label: "Сёдзё", value: 22 },
{ label: "Сёдзё-ай", value: 23 },
{ label: "Сёнэн", value: 24 },
{ label: "Сёнэн-ай", value: 25 },
{ label: "Смена пола", value: 26 },
{ label: "Современность", value: 27 },
{ label: "Спорт", value: 28 },
{ label: "Супер сила", value: 43 },
{ label: "Сэйнэн", value: 29 },
{ label: "Трагедия", value: 32 },
{ label: "Триллер", value: 33 },
{ label: "Ужасы", value: 34 },
{ label: "Уся", value: 35 },
{ label: "Фэнтези", value: 36 },
{ label: "Школьная жизнь", value: 37 },
{ label: "Этти", value: 38 },
{ label: "Юри", value: 39 },
{ label: "Яой", value: 40 },
{ label: "LitRPG", value: 41 },
],
inputType: FilterInputs.Checkbox,
},
];
-876
View File
@@ -1,876 +0,0 @@
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const defaultCoverUri =
"https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true";
const pluginId = "TL";
const sourceName = "NovelTL";
const baseUrl = "https://novel.tl";
async function popularNovels(page, { filters }) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: baseUrl,
body: JSON.stringify({
operationName: "Projects",
query:
"query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}",
variables: {
filter: {
tags: filters?.tags || [],
genres: filters?.genres || [],
},
hostname: "novel.tl",
limit: 40,
page,
},
}),
});
const json = await result.json();
let novels = [];
json?.data?.projects?.content?.forEach((novel) =>
novels.push({
name: novel.title,
url: "https://" + novel.fullUrl,
cover: novel?.covers?.[0]?.url
? baseUrl + novel.covers[0].url
: defaultCoverUri,
})
);
return novels;
}
async function parseNovelAndChapters(novelUrl) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: baseUrl,
body: JSON.stringify({
operationName: "Book",
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus fullUrl covers{url}persons(langs:["ru","en","*"],roles:["author","illustrator"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{shortName chapters{title publishDate fullUrl published}}}}}}}',
variables: {
url: novelUrl,
},
}),
});
const json = await result.json();
let novel = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project?.covers?.[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCoverUri,
summary: json.data.project?.annotation?.text,
status:
json.data.project.translationStatus === "active"
? Status.Ongoing
: Status.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item) => item?.nameRu || item?.nameEng)
.filter((item) => item);
if (genres?.length) {
novel.genres = genres.join(", ");
}
json.data.project.persons.forEach((person) => {
if (person.role == "author" && person.name.firstName) {
novel.author =
person.name.firstName + " " + (person.name?.lastName || "");
} else if (person.role == "illustrator" && person.name.firstName) {
novel.artist =
person.name.firstName + " " + (person.name?.lastName || "");
}
});
let novelChapters = [];
json.data.project.subprojects.content.forEach((work) =>
work.volumes.content.forEach((volume) =>
volume.chapters.forEach((chapter) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + " " + chapter.title,
url: "https://" + chapter.fullUrl,
releaseTime: dayjs(chapter.publishDate).format("LLL"),
})
)
)
);
novel.chapters = novelChapters;
return novel;
}
async function parseChapter(chapterUrl) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: baseUrl,
body: JSON.stringify({
query: "query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}",
variables: {
url: chapterUrl,
},
}),
});
const json = await result.json();
const loadedCheerio = cheerio.load(json.data.chapter.text.text);
loadedCheerio("p > a[href]").each(function () {
let url = baseUrl + loadedCheerio(this).attr("href");
if (!url.startsWith("http")) {
url = baseUrl + url;
}
loadedCheerio(this).find("picture").remove();
loadedCheerio(this).removeAttr("href");
loadedCheerio(`<img src="${url}">`).appendTo(this);
});
const chapterText = loadedCheerio.html();
return chapterText;
}
async function searchNovels(searchTerm) {
const result = await fetchApi(baseUrl + "/api/site/v2/graphql", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/json",
},
referrer: this.baseUrl,
body: JSON.stringify({
operationName: "Projects",
query:
"query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}",
variables: {
filter: {
query: searchTerm,
},
hostname: "novel.tl",
limit: 40,
page: 1,
},
}),
});
const json = await result.json();
let novels = [];
json?.data?.projects?.content?.forEach((novel) =>
novels.push({
name: novel.title,
url: "https://" + novel.fullUrl,
cover: novel?.covers?.[0]?.url
? baseUrl + novel.covers[0].url
: defaultCoverUri,
})
);
return novels;
}
const filters = [
{
key: "tags",
label: "Тэги",
values: [
{ label: "Автоматы", value: 64 },
{ label: "Агрессивные персонажи", value: 26 },
{ label: "Ад", value: 338 },
{ label: "Адаптация манги", value: 8 },
{ label: "Академия", value: 5 },
{ label: "Актёрское искусство", value: 7 },
{ label: "Актеры озвучки", value: 746 },
{ label: "Алхимия", value: 27 },
{ label: "Альтернативная реальность", value: 30 },
{ label: "Амнезия", value: 31 },
{ label: "Анабиоз", value: 170 },
{ label: "Анал", value: 33 },
{ label: "Ангелы", value: 38 },
{ label: "Андроиды", value: 37 },
{ label: "Анти-рост персонажа", value: 25 },
{ label: "Антигерой", value: 43 },
{ label: "Антикварный магазин", value: 44 },
{ label: "Антимагия", value: 41 },
{ label: "Антиутопия", value: 225 },
{ label: "Апатичный главный герой", value: 46 },
{ label: "Апокалипсис", value: 47 },
{ label: "Аристократия", value: 51 },
{ label: "Армия", value: 53 },
{ label: "Артефакты", value: 58 },
{ label: "Асоциальный главный герой", value: 42 },
{ label: "Ассасины", value: 61 },
{ label: "Атмосфера Европы", value: 245 },
{ label: "Банды", value: 302 },
{ label: "БДСМ", value: 77 },
{ label: "Бедный главный герой", value: 541 },
{ label: "Бездушный главный герой", value: 143 },
{ label: "Беззаботный герой", value: 115 },
{ label: "Безответная любовь", value: 213 },
{ label: "Безответная любовь", value: 740 },
{ label: "Безработные", value: 377 },
{ label: "Беременность", value: 551 },
{ label: "Бескорыстная любовь", value: 733 },
{ label: "Бессмертные", value: 359 },
{ label: "Бесстрашный главный герой", value: 274 },
{ label: "Библиотека", value: 393 },
{ label: "Биография", value: 86 },
{ label: "Близнецы", value: 729 },
{ label: "Богачи", value: 753 },
{ label: "Боги", value: 318 },
{ label: "Богини", value: 316 },
{ label: "Богоподобный протагонист", value: 314 },
{ label: "Боевая академия", value: 75 },
{ label: "Боевая парочка", value: 547 },
{ label: "Божественная защита", value: 208 },
{ label: "Божественная сила", value: 317 },
{ label: "Болеющие персонажи", value: 648 },
{ label: "Борьба за власть", value: 548 },
{ label: "Брак по расчету", value: 55 },
{ label: "Братство", value: 106 },
{ label: "Братья или сёстры", value: 646 },
{ label: "Брошенные дети", value: 1 },
{ label: "Бывший герой", value: 290 },
{ label: "Вампиры", value: 742 },
{ label: "Ваншот", value: 496 },
{ label: "Вдали от родителей", value: 3 },
{ label: "Ведьмы", value: 756 },
{ label: "Везучий главный герой", value: 411 },
{ label: "Верные подданные", value: 410 },
{ label: "Вероломные герои/плетение интриг", value: 424 },
{ label: "Взросление персонажа", value: 24 },
{ label: "Видит то, чего остальной люд не в силах", value: 617 },
{ label: "Виртуальная реальность", value: 744 },
{ label: "Владелец магазина", value: 677 },
{ label: "Владелец уникального оружия", value: 736 },
{ label: "Властные персонажи", value: 545 },
{ label: "Влияние прошлого", value: 517 },
{ label: "Внезапное обогащение", value: 691 },
{ label: "Внезапное обретение силы", value: 690 },
{ label: "Военные хроники", value: 749 },
{ label: "Возвращение в родной мир", value: 587 },
{ label: "Война", value: 750 },
{ label: "Вокалоид", value: 745 },
{ label: "Волшебные твари", value: 413 },
{ label: "Воскрешение", value: 586 },
{ label: "Воспитанный главный герой", value: 537 },
{ label: "Воспоминания прошлого", value: 283 },
{ label: "Враги становятся союзниками", value: 238 },
{ label: "Врата в иной мир", value: 303 },
{ label: "Временной парадокс", value: 711 },
{ label: "Вторжение на Землю", value: 228 },
{ label: "Второй шанс", value: 608 },
{ label: "Выживание", value: 695 },
{ label: "Вынужденные условия проживания", value: 287 },
{ label: "Высокомерный герой", value: 56 },
{ label: "Гадание", value: 207 },
{ label: "Галлюцинации", value: 189 },
{ label: "Гарем рабов", value: 656 },
{ label: "Геймеры", value: 301 },
{ label: "Генетические модификации", value: 306 },
{ label: "Герои", value: 341 },
{ label: "Герои непонятного пола", value: 36 },
{ label: "Героиня девушка-сорванец", value: 715 },
{ label: "Героиня красавица", value: 81 },
{ label: "Герой влюбляется первым", value: 561 },
{ label: "Герой использует щит", value: 636 },
{ label: "Герой красавец", value: 329 },
{ label: "Герой со множеством тел", value: 564 },
{ label: "Герой-бесстыдник", value: 632 },
{ label: "Герой-извращенец", value: 521 },
{ label: "Герой-трудяга", value: 330 },
{ label: "Герой-яндере", value: 421 },
{ label: "Гетерохромия", value: 342 },
{ label: "Гильдии", value: 324 },
{ label: "Гиперопека", value: 509 },
{ label: "Главная героиня — женщина", value: 277 },
{ label: "Главный герой - гений", value: 308 },
{ label: "Главный герой - Моб", value: 446 },
{ label: "Главный герой — знаменитость", value: 263 },
{ label: "Главный герой — мужчина", value: 419 },
{ label: "Главный герой в поисках гарема", value: 331 },
{ label: "Главный герой заранее готовится", value: 557 },
{ label: "Главный герой не человек", value: 488 },
{ label: "Главный герой приёмный", value: 20 },
{ label: "Главный герой сразу силён", value: 563 },
{ label: "Гладиаторы", value: 310 },
{ label: "Глухой к любви герой", value: 194 },
{ label: "Гоблины", value: 313 },
{ label: "Големы", value: 319 },
{ label: "Гомункул", value: 347 },
{ label: "Горничные", value: 418 },
{ label: "Государственные интриги", value: 212 },
{ label: "Гринд", value: 322 },
{ label: "Давняя травма", value: 518 },
{ label: "Двойник", value: 97 },
{ label: "Дворецкие", value: 111 },
{ label: "Дворфы", value: 224 },
{ label: "Дворяне", value: 487 },
{ label: "Двоюродные братья или сёстры", value: 162 },
{ label: "Девочки-волшебницы", value: 415 },
{ label: "Демоны", value: 193 },
{ label: "Депрессия", value: 196 },
{ label: "Детектив", value: 198 },
{ label: "Детективное расследование", value: 473 },
{ label: "Детское насилие", value: 125 },
{ label: "Детское обещание", value: 130 },
{ label: "Дискриминация", value: 203 },
{ label: "Длительный разрыв отношений", value: 400 },
{ label: "Домашняя жизнь", value: 45 },
{ label: "Драконы", value: 218 },
{ label: "Древний Китай", value: 34 },
{ label: "Дружба", value: 293 },
{ label: "Друзья детства", value: 128 },
{ label: "Друзья становятся врагами", value: 292 },
{ label: "Духи", value: 673 },
{ label: "Духовная сила", value: 665 },
{ label: "Духовный наставник", value: 671 },
{ label: "Душещипательная история", value: 335 },
{ label: "Души", value: 666 },
{ label: "Дьявольский путь культивации", value: 192 },
{ label: "Ёкай", value: 763 },
{ label: "Есть аниме-адаптация", value: 10 },
{ label: "Есть видеоигра", value: 13 },
{ label: "Есть дорама-адаптация", value: 11 },
{ label: "Есть манга-адаптация", value: 14 },
{ label: "Есть манхва-адаптация", value: 16 },
{ label: "Есть маньхуа-адаптация", value: 15 },
{ label: "Есть фильм", value: 17 },
{ label: "Есть CD дорама-адаптация", value: 12 },
{ label: "Жёсткий главный герой", value: 597 },
{ label: "Жестокие персонажи", value: 4 },
{ label: "Жёстокие персонажи", value: 169 },
{ label: "Животное-спутник", value: 78 },
{ label: "Жизнь в одиночку", value: 396 },
{ label: "Жрецы", value: 555 },
{ label: "Жрицы", value: 554 },
{ label: "Забота о детях", value: 127 },
{ label: "Заботливый главный герой", value: 116 },
{ label: "Загадочное заболевание", value: 471 },
{ label: "Загадочное прошлое родителей", value: 470 },
{ label: "Заговоры", value: 154 },
{ label: "Закалка тела", value: 96 },
{ label: "Закон джунглей", value: 682 },
{ label: "Закулисная борба", value: 603 },
{ label: "Замкнутый главный герой", value: 371 },
{ label: "Запечатанная сила", value: 607 },
{ label: "Затворник", value: 346 },
{ label: "Звери", value: 80 },
{ label: "Зверолюди", value: 39 },
{ label: "Звероподобные", value: 79 },
{ label: "Земледелие", value: 268 },
{ label: "Злобные благородные девы", value: 743 },
{ label: "Зловещие организации", value: 247 },
{ label: "Злой главный герой", value: 248 },
{ label: "Злые боги", value: 246 },
{ label: "Злые религии", value: 249 },
{ label: "Знаменитости", value: 118 },
{ label: "Знания из прошлой жизни", value: 553 },
{ label: "Зомби", value: 767 },
{ label: "Игра на выживание", value: 696 },
{ label: "Игровая рейтинговая система", value: 300 },
{ label: "Из грязи в князи", value: 542 },
{ label: "Известные родители", value: 262 },
{ label: "Изменение внешнего вида", value: 48 },
{ label: "Изнасилование", value: 574 },
{ label: "Инвалидность", value: 202 },
{ label: "Индустриализация", value: 364 },
{ label: "Инженер", value: 241 },
{ label: "Инцест", value: 361 },
{ label: "Искажённая личность", value: 730 },
{ label: "Искусственный интеллект", value: 59 },
{ label: "Исполнение желаний", value: 755 },
{ label: "Каннибализм", value: 113 },
{ label: "Карточные игры", value: 114 },
{ label: "Кладбищенский смотритель", value: 321 },
{ label: "Классика", value: 134 },
{ label: "Книги", value: 99 },
{ label: "Книги навыков", value: 654 },
{ label: "Книжный червь", value: 100 },
{ label: "Коллеги", value: 140 },
{ label: "Колледж/Университет", value: 145 },
{ label: "Командная работа", value: 701 },
{ label: "Комедийный подтекст", value: 147 },
{ label: "Комплекс брата", value: 105 },
{ label: "Комплекс неполноценности", value: 365 },
{ label: "Контракты", value: 155 },
{ label: "Контроль разума", value: 441 },
{ label: "Конфликт в семье", value: 261 },
{ label: "Конфликт верности", value: 153 },
{ label: "Копейщик", value: 668 },
{ label: "Королевская особа", value: 596 },
{ label: "Королевства", value: 382 },
{ label: "Короткий рассказ", value: 638 },
{ label: "Коррупция", value: 157 },
{ label: "Космические Войны", value: 158 },
{ label: "Космос", value: 507 },
{ label: "Косплей", value: 159 },
{ label: "Кража способностей", value: 2 },
{ label: "Крафтинг", value: 164 },
{ label: "Кризис личности", value: 357 },
{ label: "Кровь", value: 320 },
{ label: "Кружки, клубы", value: 138 },
{ label: "Кудэрэ", value: 384 },
{ label: "Кузнец", value: 90 },
{ label: "Кукловоды", value: 567 },
{ label: "Куклы/Марионетки", value: 211 },
{ label: "Кулинария", value: 156 },
{ label: "Культивация", value: 171 },
{ label: "Куннилингус", value: 172 },
{ label: "Легенды", value: 391 },
{ label: "Легкая жизнь", value: 229 },
{ label: "Лекари", value: 334 },
{ label: "Ленивый главный герой", value: 389 },
{ label: "Лидерство", value: 390 },
{ label: "Любит персонажа младше себя", value: 765 },
{ label: "Любит персонажа старше себя", value: 494 },
{ label: "Любовное соперничество", value: 406 },
{ label: "Любовные треугольники", value: 407 },
{ label: "Любовь детства", value: 129 },
{ label: "Любовь с первого взгляда", value: 404 },
{ label: "Любящие родители", value: 215 },
{ label: "Маги", value: 757 },
{ label: "Магические технологии", value: 417 },
{ label: "Магия", value: 412 },
{ label: "Магия призыва", value: 694 },
{ label: "Маленькие девочки", value: 397 },
{ label: "Мангака", value: 423 },
{ label: "Маниакальная любовь", value: 492 },
{ label: "Марти Сью", value: 508 },
{ label: "Мастер подземелий", value: 222 },
{ label: "Мастурбация", value: 432 },
{ label: "Матриархат", value: 433 },
{ label: "Межпространственные путешествия", value: 370 },
{ label: "Менеджмент", value: 422 },
{ label: "Месть", value: 588 },
{ label: "Мечи и магия", value: 697 },
{ label: "Мечники", value: 698 },
{ label: "Миленькие дети", value: 176 },
{ label: "Милитаризм", value: 439 },
{ label: "Миловидная история", value: 178 },
{ label: "Милый главный герой", value: 177 },
{ label: "Минет", value: 275 },
{ label: "Мировое древо", value: 760 },
{ label: "Миролюбивый главный герой", value: 510 },
{ label: "Мифология", value: 475 },
{ label: "Младшие сёстры", value: 766 },
{ label: "ММОРПГ", value: 445 },
{ label: "Множество временных линий", value: 463 },
{ label: "Модели", value: 447 },
{ label: "Молод не по годам", value: 49 },
{ label: "Монстры", value: 454 },
{ label: "Мрачность", value: 183 },
{ label: "Музыка", value: 466 },
{ label: "Музыкальные группы", value: 68 },
{ label: "Мутации", value: 468 },
{ label: "На волосок от смерти", value: 479 },
{ label: "На все руки мастер", value: 374 },
{ label: "Навыки имеют стоимость", value: 150 },
{ label: "Нагота", value: 490 },
{ label: "Наездники на драконах", value: 216 },
{ label: "Наёмники", value: 437 },
{ label: "Наивный главный герой", value: 476 },
{ label: "Наследие", value: 366 },
{ label: "Настоящая любовь", value: 200 },
{ label: "Настоящий гарем", value: 540 },
{ label: "Небесное испытание", value: 337 },
{ label: "Невезучий главный герой", value: 738 },
{ label: "Недоверчивый протагонист", value: 206 },
{ label: "Недооцененный главный герой", value: 734 },
{ label: "Недопонимание", value: 444 },
{ label: "Некромант", value: 480 },
{ label: "Нелинейное повествование", value: 489 },
{ label: "Неловкий главный герой", value: 67 },
{ label: "Нелюбимый протагонист", value: 333 },
{ label: "Немой персонаж", value: 469 },
{ label: "Ненадежный рассказчик", value: 739 },
{ label: "Непримечательный герой", value: 65 },
{ label: "Нерешительный протагонист", value: 363 },
{ label: "Несгибаемый главный герой", value: 676 },
{ label: "Несколько главных героев", value: 460 },
{ label: "Несколько миров", value: 461 },
{ label: "Несколько перерожденцев", value: 462 },
{ label: "Несколько попаданцев", value: 464 },
{ label: "Несравненная скорость культивации", value: 269 },
{ label: "Нехватка/отсутствие здравого смысла", value: 385 },
{ label: "Нечестный протагонист", value: 205 },
{ label: "НИИТ", value: 481 },
{ label: "Ниндзя", value: 486 },
{ label: "Обмен телами", value: 95 },
{ label: "Оборотни", value: 754 },
{ label: "Объект любви влюбился первым", value: 405 },
{ label: "Объект любви популярен", value: 543 },
{ label: "Огнестрельное оружие", value: 280 },
{ label: "Ограничение на время жизни", value: 394 },
{ label: "Одержимость", value: 544 },
{ label: "Один родитель", value: 651 },
{ label: "Одинокий главный герой", value: 399 },
{ label: "Одиночество", value: 398 },
{ label: "Одно тело на двоих", value: 634 },
{ label: "Око силы", value: 253 },
{ label: "Оммёдзи", value: 498 },
{ label: "Организованная преступность", value: 500 },
{ label: "Оргия", value: 501 },
{ label: "Орки", value: 499 },
{ label: "Основано на аниме", value: 74 },
{ label: "Основано на видеоигре", value: 72 },
{ label: "Основано на визуальной новелле", value: 73 },
{ label: "Основано на песне", value: 70 },
{ label: "Основано на фильме", value: 69 },
{ label: "Осторожный главный герой", value: 117 },
{ label: "От ненависти до любви один шаг", value: 239 },
{ label: "От сильного с сильнейшему", value: 684 },
{ label: "От слабого к сильному", value: 752 },
{ label: "Отаку", value: 503 },
{ label: "Отзывчивая любовь", value: 380 },
{ label: "Отзывчивый главный герой", value: 339 },
{ label: "Отношение учитель-ученик", value: 430 },
{ label: "Отношения бог-человек", value: 315 },
{ label: "Отношения между учителем и учеником", value: 687 },
{ label: "Отношения начальник-подчинённый", value: 101 },
{ label: "Отношения против воли", value: 286 },
{ label: "Отношения с нечеловеком", value: 353 },
{ label: "Отношения сэмпай-кохай", value: 621 },
{ label: "Отношения хозяин-слуга", value: 431 },
{ label: "Отомэ-игры", value: 504 },
{ label: "ОТП", value: 562 },
{ label: "Отшельники", value: 505 },
{ label: "Офисный работник", value: 600 },
{ label: "Официанты", value: 748 },
{ label: "Охотники", value: 355 },
{ label: "Падшие ангелы", value: 255 },
{ label: "Падшие дворяне", value: 256 },
{ label: "Пайзури", value: 511 },
{ label: "Паразиты", value: 513 },
{ label: "Параллельные миры", value: 512 },
{ label: "Парк развлечений", value: 32 },
{ label: "Пародия", value: 515 },
{ label: "Перемещение между мирами", value: 758 },
{ label: "Переодевание", value: 167 },
{ label: "Переплавка пилюль", value: 529 },
{ label: "Перерождение в девушку", value: 420 },
{ label: "Перерождение в другом мире", value: 580 },
{ label: "Перерождение в другом мире", value: 720 },
{ label: "Перерождение в игровом мире", value: 579 },
{ label: "Перерождение в монстра", value: 577 },
{ label: "Персонажи с мазохистскими наклонностями", value: 429 },
{ label: "Персонажи с садистскими наклонностями", value: 598 },
{ label: "Петля времени", value: 709 },
{ label: "Петтинг", value: 328 },
{ label: "Писатели", value: 761 },
{ label: "Питомцы", value: 522 },
{ label: "Племенное общество", value: 726 },
{ label: "Повелитель демонов", value: 191 },
{ label: "Повествование от нескольких лиц", value: 459 },
{ label: "Поглощение навыков", value: 653 },
{ label: "Подземелья", value: 223 },
{ label: "Поздно начинающаяся романтическая линия", value: 387 },
{ label: "Политика", value: 538 },
{ label: "Полулюди", value: 190 },
{ label: "Помолвка", value: 240 },
{ label: "Попаданец в другой мир", value: 723 },
{ label: "Попаданец в игровой мир", value: 722 },
{ label: "Постапокалипсис", value: 546 },
{ label: "Постижение Дао", value: 181 },
{ label: "Потеря девственности", value: 282 },
{ label: "Потустороннее пространство", value: 416 },
{ label: "Похищения", value: 379 },
{ label: "Прагматичный главный герой", value: 549 },
{ label: "Предательство", value: 83 },
{ label: "Предвидение", value: 550 },
{ label: "Преследователи", value: 674 },
{ label: "Преступление", value: 165 },
{ label: "Преступность", value: 166 },
{ label: "Приёмные дети", value: 19 },
{ label: "Призванный герой", value: 693 },
{ label: "Призраки", value: 309 },
{ label: "Приключения", value: 22 },
{ label: "Принуждение", value: 108 },
{ label: "Приручатель", value: 453 },
{ label: "Прислуга", value: 625 },
{ label: "Приставучий любовник", value: 136 },
{ label: "Притворная любовь", value: 552 },
{ label: "Пришельцы", value: 28 },
{ label: "Программист", value: 558 },
{ label: "Произведение - обладатель наград", value: 66 },
{ label: "Проклятия", value: 175 },
{ label: "Промывка мозгов", value: 102 },
{ label: "Пропуск времени", value: 712 },
{ label: "Пророчества", value: 559 },
{ label: "Протагонист в очках", value: 312 },
{ label: "Протагонист раб", value: 657 },
{ label: "Психокинез", value: 565 },
{ label: "Психопаты", value: 566 },
{ label: "Путешествия", value: 759 },
{ label: "Путешествия во времени", value: 713 },
{ label: "Пытки", value: 716 },
{ label: "Рабы", value: 658 },
{ label: "Развитие отношений", value: 160 },
{ label: "Развитие персонажа", value: 119 },
{ label: "Раздвоение личности", value: 458 },
{ label: "Размеренная романтика", value: 661 },
{ label: "Разница в статусе", value: 201 },
{ label: "Разорванная помолвка", value: 104 },
{ label: "Разумные предметы", value: 622 },
{ label: "Рай", value: 336 },
{ label: "Раняя романтика", value: 227 },
{ label: "Расизм", value: 573 },
{ label: "Расследования", value: 372 },
{ label: "Ребенок — главный герой", value: 126 },
{ label: "Реверс-гарем", value: 589 },
{ label: "Реверс-изнасилование", value: 590 },
{ label: "Резкая смена характера", value: 520 },
{ label: "Реинкарнация", value: 581 },
{ label: "Религии", value: 582 },
{ label: "Ресторан", value: 585 },
{ label: "Решительный главный герой", value: 199 },
{ label: "Робкий протагонист", value: 714 },
{ label: "Родительсткий комплекс", value: 514 },
{ label: "Родные не связанные кровью", value: 647 },
{ label: "Родословные", value: 94 },
{ label: "РПГ-система", value: 392 },
{ label: "Рыцари", value: 383 },
{ label: "Самовлюбленный главный герой", value: 477 },
{ label: "Самоубийства", value: 692 },
{ label: "Самураи", value: 601 },
{ label: "Сборник коротких историй", value: 144 },
{ label: "Свадьба", value: 426 },
{ label: "Связанные сюжетные линии", value: 369 },
{ label: "Святые", value: 599 },
{ label: "Секреты", value: 614 },
{ label: "Секс втроём", value: 707 },
{ label: "Секс-рабыня", value: 629 },
{ label: "Семейная любовь", value: 257 },
{ label: "Семь добродетелей", value: 627 },
{ label: "Семь смертных грехов", value: 626 },
{ label: "Серийные убийцы", value: 624 },
{ label: "Сикигами", value: 637 },
{ label: "Сильная любовь от старших", value: 214 },
{ label: "Синдром восьмиклассника", value: 132 },
{ label: "Синдром сестры", value: 652 },
{ label: "Сироты", value: 502 },
{ label: "Системный администратор", value: 699 },
{ label: "Скромный главный герой", value: 409 },
{ label: "Скрытые романтические отношения", value: 23 },
{ label: "Скрытые способности", value: 343 },
{ label: "Скряга", value: 450 },
{ label: "Скульпторы", value: 606 },
{ label: "Слабая романтика", value: 688 },
{ label: "Слабый главный герой", value: 751 },
{ label: "Сложные семейные отношения", value: 149 },
{ label: "Смена расы", value: 572 },
{ label: "Смертельное заболевание", value: 704 },
{ label: "Смерть", value: 185 },
{ label: "Смерть любимых", value: 186 },
{ label: "Современность", value: 448 },
{ label: "Современные знания в слаборазвитых мирах", value: 449 },
{ label: "Сожительство", value: 141 },
{ label: "Создание артефактов", value: 57 },
{ label: "Создание клана", value: 133 },
{ label: "Создание королевства", value: 381 },
{ label: "Создание навыков", value: 655 },
{ label: "Сокрытие истинных способностей", value: 344 },
{ label: "Сокрытие личности", value: 345 },
{ label: "Солдаты", value: 664 },
{ label: "Сон", value: 659 },
{ label: "Соседи по комнате", value: 595 },
{ label: "Социальные изгои", value: 663 },
{ label: "Спасение мира", value: 602 },
{ label: "Спорящая пара", value: 84 },
{ label: "Способность перевоплощения", value: 719 },
{ label: "Средневековье", value: 436 },
{ label: "Стеснительные персонажи", value: 643 },
{ label: "Стокгольмский синдром", value: 575 },
{ label: "Стокгольмский синдром", value: 675 },
{ label: "Стратег", value: 681 },
{ label: "Стратегические битвы", value: 680 },
{ label: "Стрелки", value: 325 },
{ label: "Стрельба из лука", value: 50 },
{ label: "Студенческий совет", value: 686 },
{ label: "Судьба", value: 197 },
{ label: "Суккубы", value: 689 },
{ label: "Суперспособности", value: 669 },
{ label: "Суровая тренировка", value: 332 },
{ label: "Сцены насилия", value: 195 },
{ label: "Таинственное прошлое", value: 472 },
{ label: "Тайная личность", value: 610 },
{ label: "Тайные организации", value: 611 },
{ label: "Телохранители", value: 98 },
{ label: "Тентакли", value: 703 },
{ label: "Террористы", value: 705 },
{ label: "Технологический разрыв", value: 702 },
{ label: "Тихие герои", value: 568 },
{ label: "Толстый протагонист", value: 271 },
{ label: "Торговцы", value: 438 },
{ label: "Травник", value: 340 },
{ label: "Трагичное прошлое", value: 718 },
{ label: "Трап", value: 725 },
{ label: "Триллер", value: 708 },
{ label: "Турнир", value: 76 },
{ label: "Тюрьма", value: 556 },
{ label: "Убийства", value: 465 },
{ label: "Уверенный в себе главный герой", value: 151 },
{ label: "Умная пара", value: 662 },
{ label: "Умный главный герой", value: 135 },
{ label: "Уникальная техника культивации", value: 735 },
{ label: "Уникальные оружия", value: 737 },
{ label: "Управление бизнесом", value: 109 },
{ label: "Управление временем", value: 710 },
{ label: "Управление пространством", value: 667 },
{ label: "Ускоренный рост", value: 6 },
{ label: "Учёные", value: 605 },
{ label: "Учителя", value: 700 },
{ label: "Фамильяры", value: 258 },
{ label: "Фанатизм", value: 264 },
{ label: "Фармацевт", value: 523 },
{ label: "Феи", value: 254 },
{ label: "Фениксы", value: 526 },
{ label: "Фетиш на грудь", value: 103 },
{ label: "Фетиш на очки", value: 311 },
{ label: "Философия", value: 524 },
{ label: "Фобии/Страхи", value: 525 },
{ label: "Фольклор", value: 285 },
{ label: "Формирование армии", value: 54 },
{ label: "Футанари", value: 295 },
{ label: "Футуристический мир", value: 296 },
{ label: "Фэнтези", value: 267 },
{ label: "Фэнтезийные существа", value: 266 },
{ label: "Хакеры", value: 326 },
{ label: "Харизматичный протагонист", value: 120 },
{ label: "Хитрый главный герой", value: 173 },
{ label: "Хладнокровный главный герой", value: 112 },
{ label: "Художники", value: 60 },
{ label: "Хулиганы", value: 188 },
{ label: "Цундере", value: 728 },
{ label: "Чат", value: 122 },
{ label: "Человек-оружие", value: 352 },
{ label: "Человекоподобный главный герой", value: 354 },
{ label: "Честный главный герой", value: 348 },
{ label: "Четкие любовные убеждения", value: 683 },
{ label: "Читы", value: 123 },
{ label: "Чудаковатые герои", value: 569 },
{ label: "Чужие воспоминания", value: 721 },
{ label: "Шантаж", value: 89 },
{ label: "Шеф-повар", value: 124 },
{ label: "Школа для девочек", value: 29 },
{ label: "Шоу-бизнес", value: 642 },
{ label: "Шпионы", value: 670 },
{ label: "Эволюция", value: 250 },
{ label: "Эгоистичный главный герой", value: 618 },
{ label: "Экзорцизм", value: 252 },
{ label: "Экономика", value: 230 },
{ label: "Эксгибиционизм", value: 251 },
{ label: "Эксперименты на людях", value: 351 },
{ label: "Элементальная магия", value: 234 },
{ label: "Элементы игрового мира", value: 299 },
{ label: "Элементы романтики", value: 594 },
{ label: "Элементы юри", value: 640 },
{ label: "Элементы яоя", value: 641 },
{ label: "Эльфы", value: 235 },
{ label: "Эпизодичность", value: 243 },
{ label: "Язвительные персонажи", value: 635 },
{ label: "Языковой барьер", value: 386 },
{ label: "Яндере", value: 762 },
{ label: "Яойщица", value: 294 },
{ label: "Японские силы самообороны", value: 378 },
{ label: "R-15", value: 570 },
{ label: "R-18", value: 571 },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ label: "16+", value: 1 },
{ label: "18+", value: 2 },
{ label: "Боевик", value: 3 },
{ label: "Боевые искусства", value: 4 },
{ label: "Вампиры", value: 47 },
{ label: "Военное", value: 44 },
{ label: "Гарем", value: 5 },
{ label: "Демоны", value: 48 },
{ label: "Детектив", value: 6 },
{ label: "Дзёсей", value: 50 },
{ label: "Драма", value: 7 },
{ label: "Игры", value: 45 },
{ label: "Историческое", value: 8 },
{ label: "Киберпанк", value: 9 },
{ label: "Комедия", value: 10 },
{ label: "Космос", value: 46 },
{ label: "Магия", value: 42 },
{ label: "Махо-сёдзё", value: 12 },
{ label: "Меха", value: 13 },
{ label: "Мистика", value: 14 },
{ label: "Музыка", value: 49 },
{ label: "Научная фантастика", value: 15 },
{ label: "Пародия", value: 16 },
{ label: "Повседневность", value: 17 },
{ label: "Приключения", value: 18 },
{ label: "Психологическое", value: 19 },
{ label: "Романтика", value: 20 },
{ label: "Сверхъестественное", value: 21 },
{ label: "Сёдзё", value: 22 },
{ label: "Сёдзё-ай", value: 23 },
{ label: "Сёнэн", value: 24 },
{ label: "Сёнэн-ай", value: 25 },
{ label: "Смена пола", value: 26 },
{ label: "Современность", value: 27 },
{ label: "Спорт", value: 28 },
{ label: "Супер сила", value: 43 },
{ label: "Сэйнэн", value: 29 },
{ label: "Трагедия", value: 32 },
{ label: "Триллер", value: 33 },
{ label: "Ужасы", value: 34 },
{ label: "Уся", value: 35 },
{ label: "Фэнтези", value: 36 },
{ label: "Школьная жизнь", value: 37 },
{ label: "Этти", value: 38 },
{ label: "Юри", value: 39 },
{ label: "Яой", value: 40 },
{ label: "LitRPG", value: 41 },
],
inputType: FilterInputs.Checkbox,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/noveltl/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "original.novel";
export const name = "Авторские работы";
export const icon = "";
export const version = "1.0.0";
export const site = "https://original.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Агрессивные персонажи","value":26},{"label":"Академия","value":5},{"label":"Андроиды","value":37},{"label":"Антимагия","value":41},{"label":"Аристократия","value":51},{"label":"Артефакты","value":58},{"label":"Брошенные дети","value":1},{"label":"Вероломные герои/плетение интриг","value":424},{"label":"Взросление персонажа","value":24},{"label":"Владелец уникального оружия","value":736},{"label":"Герой-извращенец","value":521},{"label":"Главный герой не человек","value":488},{"label":"Государственные интриги","value":212},{"label":"Душещипательная история","value":335},{"label":"Жестокие персонажи","value":4},{"label":"Зверолюди","value":39},{"label":"Знания из прошлой жизни","value":553},{"label":"Искусственный интеллект","value":59},{"label":"Конфликт в семье","value":261},{"label":"Королевства","value":382}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+406
View File
@@ -0,0 +1,406 @@
import cheerio from "cheerio";
import dayjs from "dayjs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { Chapter, Novel, Plugin } from "@typings/plugin";
export const id = "RLIB";
export const name = "RanobeLib";
export const site = "https://ranobelib.me";
export const version = "1.0.0";
export const icon = "src/ru/ranobelib/icon.png";
const baseUrl = site;
var ui = null;
export const popularNovels: Plugin.popularNovels = async function (
page,
{ showLatestNovels, filters }
) {
let url = `${baseUrl}/manga-list?sort=`;
url += showLatestNovels ? "last_chapter_at" : filters?.sort || "rate";
url += "&dir=" + (filters?.order || "desc");
if (filters) {
if (Array.isArray(filters.type) && filters.type.length) {
url += filters.type.map((i) => `&types[]=${i}`).join("");
}
if (Array.isArray(filters.format) && filters.format.length) {
url += filters.format
.map((i) => `&format[include][]=${i}`)
.join("");
}
if (Array.isArray(filters.status) && filters.status.length) {
url += filters.status.map((i) => `&status[]=${i}`).join("");
}
if (Array.isArray(filters.statuss) && filters.statuss.length) {
url += filters.statuss.map((i) => `&manga_status[]=${i}`).join("");
}
if (Array.isArray(filters.genres) && filters.genres.length) {
url += filters.genres
.map((i) => `&genres[include][]=${i}`)
.join("");
}
if (Array.isArray(filters.tags) && filters.tags.length) {
url += filters.tags.map((i) => `&tags[include][]=${i}`).join("");
}
}
url += "&page=" + page;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
ui = loadedCheerio("a.header-right-menu__item")
.attr("href")
?.replace?.(/[^0-9]/g, "");
let novels: Novel.Item[] = [];
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;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel: Novel.instance = {
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();
}
});
let chapters: Chapter.Item[] = [];
let chaptersJson = body.match(
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm
);
chaptersJson = chaptersJson?.[0]
?.replace("window.__DATA__ = ", "")
?.replace("window._SITE_COLOR_", "")
?.trim()
?.slice(0, -1);
chaptersJson = JSON.parse(chaptersJson);
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:
`${baseUrl}/${chaptersJson.manga.slug}/v${chapter.chapter_volume}/c${chapter.chapter_number}?bid=` +
(chapter?.branch_id || ""),
})
);
novel.chapters = chapters.reverse();
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi(chapterUrl + (ui ? `&ui=${ui}` : ""));
const body = await result.text();
const loadedCheerio = cheerio.load(body);
loadedCheerio(".reader-container img").each(function () {
const url =
loadedCheerio(this).attr("data-src") ||
loadedCheerio(this).attr("src");
if (!url?.startsWith("http")) {
loadedCheerio(this).attr("src", baseUrl + url);
} else {
loadedCheerio(this).attr("src", url);
}
loadedCheerio(this).removeAttr("data-src");
});
const chapterText = loadedCheerio(".reader-container").html();
return chapterText;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const result = await fetchApi(
`${baseUrl}/search?q=${searchTerm}&type=manga`
);
const body = await result.json();
let novels: Novel.Item[] = [];
body.forEach((novel) =>
novels.push({
name: novel?.rus_name || novel.name,
cover: novel.coverImage,
url: novel?.href || baseUrl + "/" + novel.slug,
})
);
return novels;
};
export const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Рейтинг", value: "rate" },
{ label: "Имя", value: "name" },
{ label: "Просмотры", value: "views" },
{ label: "Дате добавления", value: "created_at" },
{ label: "Дате обновления", value: "last_chapter_at" },
{ label: "Количество глав", value: "chap_count" },
],
inputType: FilterInputs.Picker,
},
{
key: "order",
label: "Порядок",
values: [
{ label: "По убыванию", value: "desc" },
{ label: "По возрастанию", value: "asc" },
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Авторский", value: "14" },
{ label: "Английский", value: "13" },
{ label: "Китай", value: "12" },
{ label: "Корея", value: "11" },
{ label: "Фанфик", value: "15" },
{ label: "Япония", value: "10" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "format",
label: "Формат выпуска",
values: [
{ label: "4-кома (Ёнкома)", value: "1" },
{ label: "В цвете", value: "4" },
{ label: "Веб", value: "6" },
{ label: "Вебтун", value: "7" },
{ label: "Додзинси", value: "3" },
{ label: "Сборник", value: "2" },
{ label: "Сингл", value: "5" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "status",
label: "Статус перевода",
values: [
{ label: "Продолжается", value: "1" },
{ label: "Завершен", value: "2" },
{ label: "Заморожен", value: "3" },
{ label: "Заброшен", value: "4" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "statuss",
label: "Статус тайтла",
values: [
{ label: "Онгоинг", value: "1" },
{ label: "Завершён", value: "2" },
{ label: "Анонс", value: "3" },
{ label: "Приостановлен", value: "4" },
{ label: "Выпуск прекращён", value: "5" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "tags",
label: "Теги",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
];
-402
View File
@@ -1,402 +0,0 @@
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const pluginId = "RLIB";
const sourceName = "RanobeLib";
const baseUrl = "https://ranobelib.me";
var ui = null;
async function popularNovels(page, { showLatestNovels, filters }) {
let url = `${baseUrl}/manga-list?sort=`;
url += showLatestNovels ? "last_chapter_at" : filters?.sort || "rate";
url += "&dir=" + (filters?.order || "desc");
if (filters?.type?.length) {
url += filters.type.map((i) => `&types[]=${i}`).join("");
}
if (filters?.format?.length) {
url += filters.format.map((i) => `&format[include][]=${i}`).join("");
}
if (filters?.status?.length) {
url += filters.status.map((i) => `&status[]=${i}`).join("");
}
if (filters?.statuss?.length) {
url += filters.statuss.map((i) => `&manga_status[]=${i}`).join("");
}
if (filters?.genres?.length) {
url += filters.genres.map((i) => `&genres[include][]=${i}`).join("");
}
if (filters?.tags?.length) {
url += filters.tags.map((i) => `&tags[include][]=${i}`).join("");
}
url += "&page=" + page;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
ui = loadedCheerio("a.header-right-menu__item")
.attr("href")
?.replace?.(/[^0-9]/g, "");
let novels = [];
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");
novels.push({ name, cover, url });
});
return novels;
}
async function parseNovelAndChapters(novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel = {
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() ===
"Продолжается"
? Status.Ongoing
: Status.Completed;
} else if (name === "Автор") {
novel.author = loadedCheerio(this).find("div:nth-child(2)").text().trim();
} else if (name === "Художник") {
novel.artist = loadedCheerio(this).find("div:nth-child(2)").text().trim();
}
});
let chapters = [];
let chaptersJson = body.match(
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm
);
chaptersJson = chaptersJson?.[0]
?.replace("window.__DATA__ = ", "")
?.replace("window._SITE_COLOR_", "")
?.trim()
?.slice(0, -1);
chaptersJson = JSON.parse(chaptersJson);
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:
`${baseUrl}/${chaptersJson.manga.slug}/v${chapter.chapter_volume}/c${chapter.chapter_number}?bid=` +
(chapter?.branch_id || ""),
})
);
novel.chapters = chapters.reverse();
return novel;
}
async function parseChapter(chapterUrl) {
const result = await fetchApi(chapterUrl + (ui ? `&ui=${ui}` : ""));
const body = await result.text();
const loadedCheerio = cheerio.load(body);
loadedCheerio(".reader-container img").each(function () {
const url =
loadedCheerio(this).attr("data-src") || loadedCheerio(this).attr("src");
if (!url?.startsWith("http")) {
loadedCheerio(this).attr("src", baseUrl + url);
} else {
loadedCheerio(this).attr("src", url);
}
loadedCheerio(this).removeAttr("data-src");
});
const chapterText = loadedCheerio(".reader-container").html();
return chapterText;
}
async function searchNovels(searchTerm) {
const result = await fetchApi(`${baseUrl}/search?q=${searchTerm}&type=manga`);
const body = await result.json();
let novels = [];
body.forEach((novel) =>
novels.push({
name: novel?.rus_name || novel.name,
cover: novel.coverImage,
url: novel?.href || baseUrl + "/" + novel.slug,
})
);
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Рейтинг", value: "rate" },
{ label: "Имя", value: "name" },
{ label: "Просмотры", value: "views" },
{ label: "Дате добавления", value: "created_at" },
{ label: "Дате обновления", value: "last_chapter_at" },
{ label: "Количество глав", value: "chap_count" },
],
inputType: FilterInputs.Picker,
},
{
key: "order",
label: "Порядок",
values: [
{ label: "По убыванию", value: "desc" },
{ label: "По возрастанию", value: "asc" },
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Авторский", value: "14" },
{ label: "Английский", value: "13" },
{ label: "Китай", value: "12" },
{ label: "Корея", value: "11" },
{ label: "Фанфик", value: "15" },
{ label: "Япония", value: "10" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "format",
label: "Формат выпуска",
values: [
{ label: "4-кома (Ёнкома)", value: "1" },
{ label: "В цвете", value: "4" },
{ label: "Веб", value: "6" },
{ label: "Вебтун", value: "7" },
{ label: "Додзинси", value: "3" },
{ label: "Сборник", value: "2" },
{ label: "Сингл", value: "5" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "status",
label: "Статус перевода",
values: [
{ label: "Продолжается", value: "1" },
{ label: "Завершен", value: "2" },
{ label: "Заморожен", value: "3" },
{ label: "Заброшен", value: "4" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "statuss",
label: "Статус тайтла",
values: [
{ label: "Онгоинг", value: "1" },
{ label: "Завершён", value: "2" },
{ label: "Анонс", value: "3" },
{ label: "Приостановлен", value: "4" },
{ label: "Выпуск прекращён", value: "5" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "tags",
label: "Теги",
values: [
{ 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" },
],
inputType: FilterInputs.Checkbox,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/ranobelib/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
+140
View File
@@ -0,0 +1,140 @@
import cheerio from "cheerio";
import dayjs from "dayjs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { Novel, Plugin } from "@typings/plugin";
export const id = "RNRF";
export const name = "РанобэРФ";
export const site = "https://ранобэ.рф";
export const version = "1.0.0";
export const icon = "src/ru/ranoberf/icon.png";
const baseUrl = site;
export const popularNovels: Plugin.popularNovels = async function (
page,
{ showLatestNovels, filters }
) {
let url = baseUrl + "/books?order=";
url += showLatestNovels
? "lastPublishedChapter"
: filters?.sort || "popular";
url += "&page=" + page;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let novels: Novel.Item[] = [];
json.props.pageProps.totalData.items.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? baseUrl + novel.verticalImage.url
: defaultCover,
url: baseUrl + "/" + novel.slug,
})
);
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const json = loadedCheerio("#__NEXT_DATA__").html();
const book = JSON.parse(json).props.pageProps.book;
let novel: Novel.instance = {
url: novelUrl,
name: book.title,
cover: book?.verticalImage?.url
? baseUrl + 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,
};
let chapters: Novel.Item[] = [];
book.chapters.forEach((chapter) => {
if (!chapter.isDonate || chapter.isUserPaid) {
chapters.push({
name: chapter.title,
releaseTime: dayjs(chapter.publishedAt).format("LLL"),
url: baseUrl + chapter.url,
});
}
});
novel.chapters = chapters.reverse();
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi(chapterUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let temp = cheerio.load(json.props.pageProps.chapter.content.text);
temp("img").each(function () {
if (!loadedCheerio(this).attr("src")?.startsWith("http")) {
const src = loadedCheerio(this).attr("src");
loadedCheerio(this).attr("src", baseUrl + src);
}
});
const chapterText = temp.html();
return chapterText;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const url = `${baseUrl}/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();
let novels: Novel.Item[] = [];
body.items.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? baseUrl + novel.verticalImage.url
: defaultCover,
url: baseUrl + "/" + novel.slug,
})
);
return novels;
};
export const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Рейтинг", value: "popular" },
{ label: "Дате добавления", value: "new" },
{ label: "Дате обновления", value: "lastPublishedChapter" },
{ label: "Законченные", value: "completed" },
],
inputType: FilterInputs.Picker,
},
];
-147
View File
@@ -1,147 +0,0 @@
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const defaultCoverUri =
"https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true";
const pluginId = "RNRF";
const sourceName = "РанобэРФ";
const baseUrl = "https://ранобэ.рф";
async function popularNovels(page, { showLatestNovels, filters }) {
let url = baseUrl + "/books?order=";
url += showLatestNovels ? "lastPublishedChapter" : filters?.sort || "popular";
url += "&page=" + page;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let novels = [];
json.props.pageProps.totalData.items.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? baseUrl + novel.verticalImage.url
: defaultCoverUri,
url: baseUrl + "/" + novel.slug,
})
);
return novels;
}
async function parseNovelAndChapters(novelUrl) {
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const json = loadedCheerio("#__NEXT_DATA__").html();
const book = JSON.parse(json).props.pageProps.book;
let novel = {
url: novelUrl,
name: book.title,
cover: book?.verticalImage?.url
? baseUrl + book.verticalImage.url
: defaultCoverUri,
summary: book.description,
author: book?.author || "",
genres: book.genres.map((item) => item.title).join(", "),
status: book.additionalInfo.includes("Активен")
? Status.Ongoing
: Status.Completed,
};
let chapters = [];
book.chapters.forEach((chapter) => {
if (!chapter.isDonate || chapter.isUserPaid) {
chapters.push({
name: chapter.title,
releaseTime: dayjs(chapter.publishedAt).format("LLL"),
url: baseUrl + chapter.url,
});
}
});
novel.chapters = chapters.reverse();
return novel;
}
async function parseChapter(chapterUrl) {
const result = await fetchApi(chapterUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let json = loadedCheerio("#__NEXT_DATA__").html();
json = JSON.parse(json);
let temp = cheerio.load(json.props.pageProps.chapter.content.text);
temp("img").each(function () {
if (!loadedCheerio(this).attr("src")?.startsWith("http")) {
const src = loadedCheerio(this).attr("src");
loadedCheerio(this).attr("src", baseUrl + src);
}
});
const chapterText = temp.html();
return chapterText;
}
async function searchNovels(searchTerm) {
const url = `${baseUrl}/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();
let novels = [];
body.items.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? baseUrl + novel.verticalImage.url
: defaultCoverUri,
url: baseUrl + "/" + novel.slug,
})
);
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Рейтинг", value: "popular" },
{ label: "Дате добавления", value: "new" },
{ label: "Дате обновления", value: "lastPublishedChapter" },
{ label: "Законченные", value: "completed" },
],
inputType: FilterInputs.Picker,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/ranoberf/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "red.novel";
export const name = "Север и Юг";
export const icon = "";
export const version = "1.0.0";
export const site = "https://red.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Героиня красавица","value":81},{"label":"Герой влюбляется первым","value":561},{"label":"Главный герой — мужчина","value":419},{"label":"Дружба","value":293},{"label":"Жизнь в одиночку","value":396},{"label":"Сложные семейные отношения","value":149}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+932
View File
@@ -0,0 +1,932 @@
import dayjs from "dayjs";
import { fetchApi } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { FilterInputs } from "@libs/filterInputs";
import { Chapter, Novel, Plugin } from "@typings/plugin";
export const id = "ReN";
export const name = "Renovels";
export const site = "https://renovels.org";
export const version = "1.0.0";
export const icon = "src/ru/renovels/icon.png";
const baseUrl = site;
export const popularNovels: Plugin.popularNovels = async function (
page,
{ showLatestNovels, filters }
) {
let url = baseUrl + "/api/search/catalog/?count=30&ordering=";
url += filters?.order ? filters?.order?.replace("+", "") : "-";
url += showLatestNovels ? "chapter_date" : filters?.sort || "rating";
if (filters?.type?.length) {
url += filters.type.map((i) => `&types=${i}`).join("");
}
if (filters?.statuss?.length) {
url += filters?.statuss.map((i) => `&status=${i}`).join("");
}
if (filters?.genres?.length) {
url += filters.genres.map((i) => `&genres=${i}`).join("");
}
if (filters?.categories?.length) {
url += filters.categories.map((i) => `&categories=${i}`).join("");
}
url += "&page=" + page;
const result = await fetchApi(url);
let body = await result.json();
let novels: Novel.Item[] = [];
body.content.forEach((novel) =>
novels.push({
name: novel.rus_name,
cover:
baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
url: baseUrl + "/novel/" + novel.dir,
})
);
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
let novelID = novelUrl.split("/")[4];
const result = await fetchApi(baseUrl + "/api/titles/" + novelID);
let body = await result.json();
let novel: Novel.instance = {
url: novelUrl,
name: body.content.rus_name,
summary: body.content.description,
cover:
baseUrl +
(body.content.img?.high ||
body.content.img?.mid ||
body.content.img.low),
status:
body.content.status.name === "Продолжается"
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let tags = []
.concat(body.content.genres, body.content.categories, [])
.map((item) => item.name);
if (tags.length > 0) {
novel.genres = tags.join(", ");
}
let all = (body.content.count_chapters / 100 + 1) ^ 0;
let chapters: Chapter.Item[] = [];
for (let i = 0; i < all; i++) {
let chapterResult = await fetchApi(
baseUrl +
"/api/titles/chapters/?branch_id=" +
body.content.branches[0].id +
"&count=100&page=" +
(i + 1)
);
let volumes = await chapterResult.json();
volumes.content.forEach((chapter) => {
if (!chapter.is_paid || chapter.is_bought) {
chapters.push({
name: `Том ${chapter.tome} Глава ${chapter.chapter} ${chapter.name}`?.trim(),
releaseTime: dayjs(chapter.upload_date).format("LLL"),
url: `${baseUrl}/novel/${novelID}/${chapter.id}/`,
});
}
});
}
novel.chapters = chapters.reverse();
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
let url = baseUrl + "/api/titles/chapters/" + chapterUrl.split("/")[5];
const result = await fetchApi(url);
const body = await result.json();
return body.content.content;
};
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
const url = `${baseUrl}/api/search/?query=${searchTerm}&count=100&field=titles`;
const result = await fetchApi(url);
let body = await result.json();
let novels: Novel.Item[] = [];
body.content.forEach((novel) =>
novels.push({
name: novel.rus_name,
cover:
baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
url: baseUrl + "/novel/" + novel.dir,
})
);
return novels;
};
export const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Рейтинг", value: "rating" },
{ label: "Просмотры", value: "views" },
{ label: "Лайкам", value: "votes" },
{ label: "Дате добавления", value: "id" },
{ label: "Дате обновления", value: "chapter_date" },
{ label: "Количество глав", value: "count_chapters" },
],
inputType: FilterInputs.Picker,
},
{
key: "order",
label: "Порядок",
values: [
{ label: "По убыванию", value: "-" }, // desc
{ label: "По возрастанию", value: "+" }, // asc
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Авторское", value: "1" },
{ label: "Другое", value: "7" },
{ label: "Запад", value: "5" },
{ label: "Китай", value: "4" },
{ label: "Корея", value: "3" },
{ label: "Фанфики", value: "6" },
{ label: "Япония", value: "2" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "statuss",
label: "Статус тайтла",
values: [
{ label: "Закончен", value: "0" },
{ label: "Продолжается", value: "1" },
{ label: "Заморожен", value: "2" },
{ label: "Нет переводчика", value: "3" },
{ label: "Анонс", value: "4" },
{ label: "Лицензировано", value: "5" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ label: "Боевик", value: "112" },
{ label: "Война", value: "123" },
{ label: "Детектив", value: "114" },
{ label: "Драма", value: "125" },
{ label: "Историческая проза", value: "115" },
{ label: "ЛитРПГ", value: "109" },
{ label: "Любовные романы", value: "116" },
{ label: "Мистика", value: "103" },
{ label: "Научная фантастика", value: "121" },
{ label: "Повседневность", value: "128" },
{ label: "Подростковая проза", value: "113" },
{ label: "Политический роман", value: "119" },
{ label: "Попаданцы", value: "108" },
{ label: "Поэзия", value: "105" },
{ label: "Разное", value: "111" },
{ label: "РеалРПГ", value: "117" },
{ label: "Романтика", value: "126" },
{ label: "Современная проза", value: "102" },
{ label: "Современная фантастика", value: "127" },
{ label: "Спорт", value: "122" },
{ label: "Трагедия", value: "129" },
{ label: "Триллер", value: "110" },
{ label: "Триллер и саспенс", value: "124" },
{ label: "Ужасы", value: "106" },
{ label: "Фантастика", value: "101" },
{ label: "Фанфик", value: "107" },
{ label: "Фьюжн роман", value: "120" },
{ label: "Фэнтези", value: "100" },
{ label: "Эротика", value: "118" },
{ label: "Юмор", value: "104" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "categories",
label: "Категории",
values: [
{ label: "[Награжденная работа]", value: "648" },
{ label: "18+", value: "423" },
{ label: "Абьюзеры", value: "663" },
{ label: "Авантюристы", value: "436" },
{ label: "Автоматоны", value: "593" },
{ label: "Агрессивные персонажи", value: "502" },
{ label: "Ад", value: "569" },
{ label: "Адаптация в радиопостановку", value: "581" },
{ label: "Академия", value: "153" },
{ label: "Актеры озвучки", value: "627" },
{ label: "Активный главный герой", value: "251" },
{ label: "Алхимия", value: "233" },
{ label: "Альтернативная история", value: "147" },
{ label: "Альтернативный мир", value: "156" },
{ label: "Амнезия/Потеря памяти", value: "344" },
{ label: "Анабиоз", value: "685" },
{ label: "Ангелы", value: "183" },
{ label: "Андрогинные персонажи", value: "321" },
{ label: "Андроиды", value: "203" },
{ label: "Анти-магия", value: "533" },
{ label: "Антигерой", value: "432" },
{ label: "Антикварный магазин", value: "623" },
{ label: "Антисоциальный главный герой", value: "613" },
{ label: "Антиутопия", value: "142" },
{ label: "Апатичный протагонист", value: "157" },
{ label: "Апокалипсис", value: "402" },
{ label: "Аранжированный брак", value: "375" },
{ label: "Армия", value: "639" },
{ label: "Артефакты", value: "234" },
{ label: "Артисты", value: "523" },
{ label: "Банды", value: "630" },
{ label: "БДСМ", value: "699" },
{ label: "Бедный главный герой", value: "398" },
{ label: "Безжалостный главный герой", value: "261" },
{ label: "Беззаботный главный герой", value: "438" },
{ label: "Безусловная любовь", value: "679" },
{ label: "Беременность", value: "250" },
{ label: "Бесполый главный герой", value: "325" },
{ label: "Бессмертные", value: "368" },
{ label: "Бесстрашный протагонист", value: "656" },
{ label: "Бесстыдный главный герой", value: "352" },
{ label: "Бесчестный главный герой", value: "716" },
{ label: "Библиотека", value: "428" },
{ label: "Бизнес-литература", value: "121" },
{ label: "Бизнесмен", value: "769" },
{ label: "Биочип", value: "238" },
{ label: "Бисексуальный главный герой", value: "773" },
{ label: "Близнецы", value: "263" },
{ label: "Боги", value: "317" },
{ label: "Богини", value: "439" },
{ label: "Боевая академия", value: "450" },
{ label: "Боевая фантастика", value: "149" },
{ label: "Боевое фэнтези", value: "104" },
{ label: "Боевые духи", value: "433" },
{ label: "Боевые соревнования", value: "493" },
{ label: "Божественная защита", value: "421" },
{ label: "Божественные силы", value: "327" },
{ label: "Борьба за власть", value: "599" },
{ label: "Брак", value: "446" },
{ label: "Брак по расчету", value: "188" },
{ label: "Братский комплекс", value: "159" },
{ label: "Братство", value: "487" },
{ label: "Братья и сестры", value: "577" },
{ label: "Буддизм", value: "745" },
{ label: "Быстрая культивация", value: "366" },
{ label: "Быстрообучаемый", value: "324" },
{ label: "Валькирии", value: "692" },
{ label: "Вампиры", value: "360" },
{ label: "Ваншот", value: "702" },
{ label: "Ведьмы", value: "281" },
{ label: "Вежливый главный герой", value: "379" },
{ label: "Верные подчиненные", value: "328" },
{ label: "Взрослый главный герой", value: "295" },
{ label: "Видит то, чего не видят другие", value: "671" },
{ label: "Виртуальная реальность", value: "401" },
{ label: "Владелец магазина", value: "681" },
{ label: "Внезапная сила", value: "455" },
{ label: "Внезапное богатство", value: "765" },
{ label: "Внешний вид отличается от факт", value: "419" },
{ label: "Военные Летописи", value: "743" },
{ label: "Возвращение из другого мира", value: "696" },
{ label: "Войны", value: "181" },
{ label: "Вокалоид", value: "701" },
{ label: "Волшебники/Волшебницы", value: "539" },
{ label: "Волшебные звери", value: "308" },
{ label: "Воображаемый друг", value: "651" },
{ label: "Воры", value: "414" },
{ label: "Воскрешение", value: "199" },
{ label: "Враги становятся возлюбленными", value: "498" },
{ label: "Враги становятся союзниками", value: "562" },
{ label: "Врата в другой мир", value: "609" },
{ label: "Врачи", value: "376" },
{ label: "Временной парадокс", value: "275" },
{ label: "Всемогущий главный герой", value: "169" },
{ label: "Вторжение на землю", value: "198" },
{ label: "Второй шанс", value: "231" },
{ label: "Вуайеризм", value: "790" },
{ label: "Выживание", value: "380" },
{ label: "Высокомерные персонажи", value: "361" },
{ label: "Гадание", value: "595" },
{ label: "Гарем рабов", value: "777" },
{ label: "Гг - женщина", value: "186" },
{ label: "Гг - мужчина", value: "166" },
{ label: "Гг силен с самого начала", value: "171" },
{ label: "Геймеры", value: "392" },
{ label: "Генералы", value: "326" },
{ label: "Генетические модификации", value: "657" },
{ label: "Гениальный главный герой", value: "617" },
{ label: "Герои", value: "285" },
{ label: "Героиня — сорванец", value: "583" },
{ label: "Героическая фантастика", value: "140" },
{ label: "Героическое фэнтези", value: "107" },
{ label: "Герой влюбляется первым", value: "187" },
{ label: "Гетерохромия", value: "570" },
{ label: "Гильдии", value: "411" },
{ label: "Гипнотизм", value: "755" },
{ label: "Главный герой — бог", value: "547" },
{ label: "Главный герой — гуманоид", value: "636" },
{ label: "Главный герой — наполовину чел", value: "445" },
{ label: "Главный герой — отец", value: "792" },
{ label: "Главный герой — раб", value: "780" },
{ label: "Главный герой — ребенок", value: "488" },
{ label: "Главный герой — рубака", value: "476" },
{ label: "Главный герой влюбляется первы", value: "683" },
{ label: "Главный герой играет роль", value: "472" },
{ label: "Главный герой носит очки", value: "672" },
{ label: "Главный герой пацифист", value: "698" },
{ label: "Гладиаторы", value: "603" },
{ label: "Глуповатый главный герой", value: "385" },
{ label: "Гоблины", value: "586" },
{ label: "Големы", value: "620" },
{ label: "Гомункул", value: "789" },
{ label: "Горничные", value: "459" },
{ label: "Городское фэнтези", value: "105" },
{ label: "Госпиталь", value: "800" },
{ label: "Готовка", value: "302" },
{ label: "Гриндинг", value: "393" },
{ label: "Дао Компаньон", value: "462" },
{ label: "Даосизм", value: "760" },
{ label: "Дварфы", value: "323" },
{ label: "Двойная личность", value: "642" },
{ label: "Двойник", value: "601" },
{ label: "Дворецкий", value: "660" },
{ label: "Дворяне", value: "168" },
{ label: "Дворянство/Аристократия", value: "437" },
{ label: "Девушки-монстры", value: "669" },
{ label: "Демоническая техника культивац", value: "722" },
{ label: "Демоны", value: "152" },
{ label: "Денежный долг", value: "783" },
{ label: "Депрессия", value: "555" },
{ label: "Детектив", value: "116" },
{ label: "Детективы", value: "612" },
{ label: "Детская литература", value: "123" },
{ label: "Дискриминация", value: "162" },
{ label: "Добыча денег в приоритете", value: "396" },
{ label: "Документальная проза", value: "122" },
{ label: "Долгая разлука", value: "307" },
{ label: "Домашние дела", value: "290" },
{ label: "Домогательство", value: "470" },
{ label: "Драконы", value: "303" },
{ label: "Драконьи всадники", value: "607" },
{ label: "Древние времена", value: "221" },
{ label: "Древний Китай", value: "374" },
{ label: "Дружба", value: "217" },
{ label: "Друзья детства", value: "282" },
{ label: "Друзья становятся врагами", value: "567" },
{ label: "Друиды", value: "497" },
{ label: "Дух лисы", value: "784" },
{ label: "Духи/призраки", value: "172" },
{ label: "Духовный советник", value: "435" },
{ label: "Душевность", value: "633" },
{ label: "Души", value: "254" },
{ label: "Европейская атмосфера", value: "521" },
{ label: "Ёкаи", value: "575" },
{ label: "Есть аниме-адаптация", value: "154" },
{ label: "Есть видеоигра по мотивам", value: "552" },
{ label: "Есть манга", value: "155" },
{ label: "Есть манхва-адаптация", value: "517" },
{ label: "Есть маньхуа-адаптация", value: "388" },
{ label: "Есть сериал-адаптация", value: "492" },
{ label: "Есть фильм", value: "173" },
{ label: "Женища-наставник", value: "505" },
{ label: "Жесткая, двуличная личность", value: "346" },
{ label: "Жестокие персонажи", value: "503" },
{ label: "Жестокое обращение с ребенком", value: "654" },
{ label: "Жестокость", value: "245" },
{ label: "Животноводство", value: "754" },
{ label: "Животные черты", value: "528" },
{ label: "Жизнь в квартире", value: "606" },
{ label: "Жрицы", value: "615" },
{ label: "Заботливый главный герой", value: "288" },
{ label: "Забывчивый главный герой", value: "628" },
{ label: "Заговоры", value: "289" },
{ label: "Закалка тела", value: "362" },
{ label: "Законники", value: "756" },
{ label: "Замкнутый главный герой", value: "589" },
{ label: "Запечатанная сила", value: "430" },
{ label: "Застенчивые персонажи", value: "508" },
{ label: "Звери", value: "237" },
{ label: "Звери-компаньоны", value: "301" },
{ label: "Злой протагонист", value: "243" },
{ label: "Злые боги", value: "504" },
{ label: "Злые организации", value: "563" },
{ label: "Злые религии", value: "734" },
{ label: "Знаменитости", value: "473" },
{ label: "Знаменитый главный герой", value: "531" },
{ label: "Знания современного мира", value: "297" },
{ label: "Зомби", value: "409" },
{ label: "Игра на выживание", value: "274" },
{ label: "Игривый протагонист", value: "717" },
{ label: "Игровая система рейтинга", value: "391" },
{ label: "Игровые элементы", value: "266" },
{ label: "Игрушки (18+)", value: "676" },
{ label: "Из грязи в князи", value: "416" },
{ label: "Из женщины в мужчину", value: "709" },
{ label: "Из мужчины в женщину", value: "715" },
{ label: "Из полного в худого", value: "767" },
{ label: "Из слабого в сильного", value: "202" },
{ label: "Извращенный главный герой", value: "434" },
{ label: "Изгои", value: "534" },
{ label: "Изменение расы", value: "664" },
{ label: "Изменения внешнего вида", value: "300" },
{ label: "Изменения личности", value: "219" },
{ label: "Изнасилование", value: "229" },
{ label: "Изображения жестокости", value: "355" },
{ label: "ИИ", value: "235" },
{ label: "Империи", value: "748" },
{ label: "Инвалидность", value: "684" },
{ label: "Индустриализация", value: "292" },
{ label: "Инженер", value: "163" },
{ label: "Инцест", value: "525" },
{ label: "Искусственный интеллект", value: "236" },
{ label: "Исследования", value: "670" },
{ label: "Историческая проза", value: "119" },
{ label: "Исторические приключения", value: "120" },
{ label: "Исторический детектив", value: "117" },
{ label: "Исторический любовный роман", value: "138" },
{ label: "Историческое фэнтези", value: "110" },
{ label: "Каннибализм", value: "456" },
{ label: "Карточные игры", value: "682" },
{ label: "Киберпанк", value: "141" },
{ label: "Киберспорт", value: "686" },
{ label: "Кланы", value: "796" },
{ label: "Класс безработного", value: "735" },
{ label: "Клоны", value: "447" },
{ label: "Клубы", value: "216" },
{ label: "Книги", value: "427" },
{ label: "Книги навыков", value: "400" },
{ label: "Книжный червь", value: "519" },
{ label: "Коварство", value: "230" },
{ label: "Коллеги", value: "704" },
{ label: "Колледж/Университет", value: "726" },
{ label: "Кома", value: "776" },
{ label: "Командная работа", value: "496" },
{ label: "Комедийный оттенок", value: "582" },
{ label: "Комплекс неполноценности", value: "550" },
{ label: "Комплекс семейных отношений", value: "223" },
{ label: "Конкуренция", value: "746" },
{ label: "Контроль разума/сознания", value: "357" },
{ label: "Копейщик", value: "425" },
{ label: "Королевская битва", value: "797" },
{ label: "Королевская власть", value: "178" },
{ label: "Королевства", value: "259" },
{ label: "Короткий любовный роман", value: "136" },
{ label: "Коррупция", value: "457" },
{ label: "Космическая фантастика", value: "146" },
{ label: "Космические войны", value: "697" },
{ label: "Красивый герой", value: "226" },
{ label: "Крафт", value: "377" },
{ label: "Кризис личности", value: "640" },
{ label: "Кругосветное путешествие", value: "353" },
{ label: "Кудере", value: "506" },
{ label: "Кузены", value: "718" },
{ label: "Кузнец", value: "518" },
{ label: "Кукловоды", value: "501" },
{ label: "Куклы/марионетки", value: "624" },
{ label: "Культивация", value: "241" },
{ label: "Куннилингус", value: "667" },
{ label: "Легенды", value: "500" },
{ label: "Легкая жизнь", value: "644" },
{ label: "Ленивый главный герой", value: "621" },
{ label: "Лидерство", value: "494" },
{ label: "Лоли", value: "213" },
{ label: "Лотерея", value: "477" },
{ label: "Любовное фэнтези", value: "135" },
{ label: "Любовный треугольник", value: "218" },
{ label: "Любовь детства", value: "560" },
{ label: "Любовь с первого взгляда", value: "737" },
{ label: "Магические надписи", value: "453" },
{ label: "Магические печати", value: "369" },
{ label: "Магические технологии", value: "440" },
{ label: "Магия", value: "165" },
{ label: "Магия призыва", value: "418" },
{ label: "Мазохистские персонажи", value: "688" },
{ label: "Манипулятивные персонажи", value: "249" },
{ label: "Мания", value: "212" },
{ label: "Мастер на все руки", value: "466" },
{ label: "Мастурбация", value: "788" },
{ label: "Махо-сёдзё", value: "556" },
{ label: "Медицинские знания", value: "507" },
{ label: "Медленная романтическая линия", value: "232" },
{ label: "Медленное развитие на старте", value: "694" },
{ label: "Межпространственные путешестви", value: "404" },
{ label: "Менеджмент", value: "294" },
{ label: "Мертвый главный герой", value: "761" },
{ label: "Месть", value: "209" },
{ label: "Метаморфы", value: "335" },
{ label: "Меч и магия", value: "180" },
{ label: "Мечник", value: "647" },
{ label: "Мечты", value: "738" },
{ label: "Милая история", value: "725" },
{ label: "Милое дитя", value: "637" },
{ label: "Милый главный герой", value: "714" },
{ label: "Мировое дерево", value: "463" },
{ label: "Мистика", value: "151" },
{ label: "Мистический ореол вокруг семьи", value: "483" },
{ label: "Мифические звери", value: "491" },
{ label: "Мифология", value: "530" },
{ label: "Младшие братья", value: "785" },
{ label: "Младшие сестры", value: "527" },
{ label: "ММОРПГ (ЛитРПГ)", value: "395" },
{ label: "Множество перемещенных людей", value: "549" },
{ label: "Множество реальностей", value: "370" },
{ label: "Множество реинкарнированных лю", value: "330" },
{ label: "Модели", value: "678" },
{ label: "Молчаливый персонаж", value: "721" },
{ label: "Монстры", value: "192" },
{ label: "Мужская гей-пара", value: "706" },
{ label: "Мужчина-яндере", value: "267" },
{ label: "Музыка", value: "635" },
{ label: "Музыкальные группы", value: "634" },
{ label: "Мутации", value: "693" },
{ label: "Мутированные существа", value: "405" },
{ label: "Навык кражи", value: "662" },
{ label: "Навязчивая любовь", value: "206" },
{ label: "Наемники", value: "412" },
{ label: "Назойливый возлюбленный", value: "561" },
{ label: "Наивный главный герой", value: "189" },
{ label: "Наркотики", value: "689" },
{ label: "Нарциссический главный герой", value: "532" },
{ label: "Насилие сексуального характера", value: "774" },
{ label: "Наследование", value: "452" },
{ label: "Научная фантастика", value: "144" },
{ label: "Национализм", value: "406" },
{ label: "Не блещущий внешне главный гер", value: "415" },
{ label: "Не родные братья и сестры", value: "526" },
{ label: "Небеса", value: "568" },
{ label: "Небесное испытание", value: "367" },
{ label: "Негуманоидный главный герой", value: "659" },
{ label: "Недоверчивый главный герой", value: "546" },
{ label: "Недооцененный главный герой", value: "258" },
{ label: "Недоразумения", value: "309" },
{ label: "Неизлечимая болезнь", value: "196" },
{ label: "Некромант", value: "397" },
{ label: "Нелинейная история", value: "269" },
{ label: "Ненавистный главный герой", value: "548" },
{ label: "Ненадежный рассказчик", value: "277" },
{ label: "Нерезиденты", value: "772" },
{ label: "Нерешительный главный герой", value: "744" },
{ label: "Несерьезный главный герой", value: "384" },
{ label: "Несколько временных линий", value: "576" },
{ label: "Несколько гг", value: "268" },
{ label: "Несколько главных героев", value: "537" },
{ label: "Несколько идентичностей", value: "652" },
{ label: "Несколько личностей", value: "536" },
{ label: "Нетораре", value: "732" },
{ label: "Нетори", value: "514" },
{ label: "Неудачливый главный герой", value: "618" },
{ label: "Ниндзя", value: "441" },
{ label: "Новелла", value: "103" },
{ label: "Обещание из детства", value: "742" },
{ label: "Обманщик", value: "485" },
{ label: "Обмен телами", value: "214" },
{ label: "Обнаженка", value: "490" },
{ label: "Обольщение", value: "731" },
{ label: "Оборотни", value: "661" },
{ label: "Обратный гарем", value: "351" },
{ label: "Общество монстров", value: "329" },
{ label: "Обязательство", value: "602" },
{ label: "Огнестрельное оружие", value: "291" },
{ label: "Ограниченное время жизни", value: "194" },
{ label: "Одержимость", value: "512" },
{ label: "Одинокий главный герой", value: "394" },
{ label: "Одиночество", value: "306" },
{ label: "Одиночное проживание", value: "313" },
{ label: "Околосмертные переживания", value: "759" },
{ label: "Оммёдзи", value: "597" },
{ label: "Омоложение", value: "338" },
{ label: "Организованная преступность", value: "540" },
{ label: "Оргия", value: "775" },
{ label: "Орки", value: "331" },
{ label: "Освоение навыков", value: "336" },
{ label: "Основано на аниме", value: "605" },
{ label: "Основано на видео игре", value: "720" },
{ label: "Основано на визуальной новелле", value: "793" },
{ label: "Основано на песне", value: "700" },
{ label: "Основано на фильме", value: "604" },
{ label: "Осторожный главный герой", value: "240" },
{ label: "Отаку", value: "571" },
{ label: "Открытый космос", value: "264" },
{ label: "Отношения в сети", value: "727" },
{ label: "Отношения между людьми и нелюд", value: "311" },
{ label: "Отношения на расстоянии", value: "791" },
{ label: "Отношения Сенпай-Коухай", value: "572" },
{ label: "Отношения ученика и учителя", value: "515" },
{ label: "Отношения учитель-ученик", value: "429" },
{ label: "Отношения хозяин-слуга", value: "312" },
{ label: "Отомэ игра", value: "733" },
{ label: "Отсутствие здравого смысла", value: "645" },
{ label: "Отсутствие родителей", value: "625" },
{ label: "Офисный роман", value: "707" },
{ label: "Официанты", value: "703" },
{ label: "Охотники", value: "378" },
{ label: "Очаровательный главный герой", value: "687" },
{ label: "Падшее дворянство", value: "655" },
{ label: "Падшие ангелы", value: "565" },
{ label: "Пайзури", value: "665" },
{ label: "Паразиты", value: "591" },
{ label: "Параллельные миры", value: "207" },
{ label: "Парк", value: "265" },
{ label: "Парк развлечений", value: "632" },
{ label: "Пародия", value: "407" },
{ label: "Певцы/Певицы", value: "739" },
{ label: "Первая любовь", value: "729" },
{ label: "Первоисточник новеллы — манга", value: "611" },
{ label: "Первый раз", value: "787" },
{ label: "Перемещение в игровой мир", value: "588" },
{ label: "Перемещение в иной мир", value: "750" },
{ label: "Перерождение в ином мире", value: "751" },
{ label: "Переселение души/трансмиграция", value: "257" },
{ label: "Персонаж использует щит", value: "666" },
{ label: "Петля времени", value: "200" },
{ label: "Пираты", value: "771" },
{ label: "Писатели", value: "482" },
{ label: "Питомцы", value: "349" },
{ label: "Племенное общество", value: "381" },
{ label: "Повелитель демонов", value: "283" },
{ label: "Подземелья", value: "322" },
{ label: "Подростковая проза", value: "114" },
{ label: "Пожелания", value: "278" },
{ label: "Познание Дао", value: "443" },
{ label: "Покинутое дитя", value: "590" },
{ label: "Полигамия", value: "386" },
{ label: "Политика", value: "170" },
{ label: "Политический роман", value: "133" },
{ label: "Полиция", value: "764" },
{ label: "Полулюди", value: "420" },
{ label: "Популярный любовный интерес", value: "342" },
{ label: "Постапокалипсис", value: "145" },
{ label: "Постапокалиптика", value: "184" },
{ label: "Потерянные цивилизации", value: "749" },
{ label: "Похищения людей", value: "723" },
{ label: "Поэзия", value: "111" },
{ label: "Правонарушители", value: "713" },
{ label: "Прагматичный главный герой", value: "592" },
{ label: "Преданный любовный интерес", value: "225" },
{ label: "Предательство", value: "222" },
{ label: "Предвидение", value: "177" },
{ label: "Прекрасная героиня", value: "158" },
{ label: "Преступники", value: "736" },
{ label: "Преступность", value: "204" },
{ label: "Призванный герой", value: "262" },
{ label: "Призраки", value: "176" },
{ label: "Принуждение к отношениям", value: "643" },
{ label: "Принцессы", value: "795" },
{ label: "Притворная пара", value: "641" },
{ label: "Причудливые персонажи", value: "387" },
{ label: "Пришельцы/инопланетяне", value: "197" },
{ label: "Программист", value: "691" },
{ label: "Проклятия", value: "174" },
{ label: "Промывание мозгов", value: "578" },
{ label: "Пропуск времени", value: "256" },
{ label: "Пророчества", value: "499" },
{ label: "Проститутки", value: "786" },
{ label: "Прошлое играет большую роль", value: "320" },
{ label: "Прыжки между мирами", value: "741" },
{ label: "Психические силы", value: "359" },
{ label: "Психопаты", value: "270" },
{ label: "Публицистика", value: "124" },
{ label: "Путешествие во времени", value: "201" },
{ label: "Пытка", value: "280" },
{ label: "Рабы", value: "778" },
{ label: "Развитие личности", value: "125" },
{ label: "Развод", value: "758" },
{ label: "Разумные предметы", value: "286" },
{ label: "Расизм", value: "408" },
{ label: "Рассказ", value: "185" },
{ label: "Расторжения помолвки", value: "343" },
{ label: "Расы зооморфов", value: "315" },
{ label: "Реализм", value: "246" },
{ label: "Ревность", value: "730" },
{ label: "Редакторы", value: "705" },
{ label: "Реинкарнация", value: "372" },
{ label: "Реинкарнация в монстра", value: "310" },
{ label: "Реинкарнация в объект", value: "711" },
{ label: "Религии", value: "616" },
{ label: "Репортеры", value: "781" },
{ label: "Ресторан", value: "680" },
{ label: "Решительный главный герой", value: "242" },
{ label: "Робкий главный герой", value: "763" },
{ label: "Родитель одиночка", value: "708" },
{ label: "Родительский комплекс", value: "587" },
{ label: "Родословная", value: "239" },
{ label: "Романтическая эротика", value: "132" },
{ label: "Романтический подсюжет", value: "271" },
{ label: "Рост персонажа", value: "215" },
{ label: "Рыцари", value: "260" },
{ label: "Садистские персонажи", value: "674" },
{ label: "Самоотверженный главный герой", value: "747" },
{ label: "Самурай", value: "677" },
{ label: "Сборник коротких историй", value: "520" },
{ label: "Связанные сюжетные линии", value: "535" },
{ label: "Святые", value: "752" },
{ label: "Священники", value: "413" },
{ label: "Сдержанный главный герой", value: "762" },
{ label: "Секретные организации", value: "417" },
{ label: "Секреты", value: "626" },
{ label: "Секс рабы", value: "779" },
{ label: "Семейный конфликт", value: "757" },
{ label: "Семь добродетелей", value: "334" },
{ label: "Семь смертных грехов", value: "253" },
{ label: "Семья", value: "347" },
{ label: "Сёнэн-ай подсюжет", value: "690" },
{ label: "Серийные убийцы", value: "557" },
{ label: "Сестринский комплекс", value: "558" },
{ label: "Сила духа", value: "373" },
{ label: "Сила, требующая платы за польз", value: "579" },
{ label: "Сильная пара", value: "228" },
{ label: "Сильный в сильнейшего", value: "442" },
{ label: "Сильный любовный интерес", value: "273" },
{ label: "Синдром восьмиклассника", value: "211" },
{ label: "Сироты", value: "319" },
{ label: "Система уровней", value: "305" },
{ label: "Системный администратор", value: "538" },
{ label: "Сказка", value: "126" },
{ label: "Скрытие истинной личности", value: "451" },
{ label: "Скрытие истинных способностей", value: "348" },
{ label: "Скрытный главный герой", value: "252" },
{ label: "Скрытые способности", value: "247" },
{ label: "Скульпторы", value: "448" },
{ label: "Слабо выраженная романтическая", value: "318" },
{ label: "Слабый главный герой", value: "298" },
{ label: "Слепой главный герой", value: "794" },
{ label: "Слуги", value: "333" },
{ label: "Слэш", value: "131" },
{ label: "Смерть", value: "175" },
{ label: "Смерть близких", value: "444" },
{ label: "Собственнические персонажи", value: "227" },
{ label: "Современная проза", value: "150" },
{ label: "Современность", value: "167" },
{ label: "Современный любовный роман", value: "137" },
{ label: "Сожительство", value: "544" },
{ label: "Создание армии", value: "287" },
{ label: "Создание артефактов", value: "382" },
{ label: "Создание клана", value: "513" },
{ label: "Создание королевства", value: "293" },
{ label: "Создание навыков", value: "337" },
{ label: "Создание секты", value: "469" },
{ label: "Солдаты/военные", value: "193" },
{ label: "Сон", value: "622" },
{ label: "Состоятельные персонажи", value: "190" },
{ label: "Социальная иерархия по силе", value: "255" },
{ label: "Социальная фантастика", value: "148" },
{ label: "Социальные изгои", value: "542" },
{ label: "Спасение мира", value: "638" },
{ label: "Специальные способности", value: "179" },
{ label: "Спокойный главный герой", value: "160" },
{ label: "Справедливый главный герой", value: "719" },
{ label: "Средневековье", value: "296" },
{ label: "Ссорящаяся пара", value: "383" },
{ label: "Сталкеры", value: "710" },
{ label: "Старение", value: "299" },
{ label: "Стимпанк", value: "139" },
{ label: "Стоические персонажи", value: "509" },
{ label: "Стокгольмский синдром", value: "675" },
{ label: "Стратег", value: "495" },
{ label: "Стратегические битвы", value: "272" },
{ label: "Стратегия", value: "798" },
{ label: "Стрелки", value: "522" },
{ label: "Стрельба из лука", value: "461" },
{ label: "Студенческий совет", value: "551" },
{ label: "Судьба", value: "364" },
{ label: "Суккубы", value: "545" },
{ label: "Супер герои", value: "799" },
{ label: "Суровая подготовка", value: "356" },
{ label: "Таинственная болезнь", value: "195" },
{ label: "Таинственное прошлое", value: "358" },
{ label: "Тайная личность", value: "399" },
{ label: "Тайные отношения", value: "768" },
{ label: "Танцоры", value: "782" },
{ label: "Телохранители", value: "516" },
{ label: "Темное фэнтези", value: "106" },
{ label: "Тентакли", value: "712" },
{ label: "Террористы", value: "574" },
{ label: "Технологический разрыв", value: "658" },
{ label: "Тихие персонажи", value: "600" },
{ label: "Толстый главный герой", value: "389" },
{ label: "Торговцы", value: "489" },
{ label: "Травля/буллинг", value: "210" },
{ label: "Травник", value: "724" },
{ label: "Трагическое прошлое", value: "276" },
{ label: "Трансплантация воспоминаний", value: "449" },
{ label: "Триллер", value: "134" },
{ label: "Трудолюбивый главный герой", value: "164" },
{ label: "Тюрьма", value: "541" },
{ label: "Убийства", value: "205" },
{ label: "Убийцы", value: "345" },
{ label: "Убийцы драконов", value: "646" },
{ label: "Уверенный главный герой", value: "363" },
{ label: "Удачливый главный герой", value: "478" },
{ label: "Ужасы", value: "112" },
{ label: "Укротитель монстров", value: "422" },
{ label: "Умения из прошлой жизни", value: "371" },
{ label: "Умная пара", value: "554" },
{ label: "Умный главный герой", value: "161" },
{ label: "Уникальная техника Культивации", value: "350" },
{ label: "Уникальное оружие", value: "426" },
{ label: "Управление бизнесом", value: "403" },
{ label: "Управление временем", value: "279" },
{ label: "Управление кровью", value: "753" },
{ label: "Упрямый главный герой", value: "695" },
{ label: "Уродливый главный герой", value: "766" },
{ label: "Ускоренный рост", value: "390" },
{ label: "Усыновленные дети", value: "543" },
{ label: "Усыновленный главный герой", value: "486" },
{ label: "Уход за детьми", value: "474" },
{ label: "Учителя", value: "431" },
{ label: "Фамильяры", value: "596" },
{ label: "Фанатизм", value: "650" },
{ label: "Фантастические существа", value: "410" },
{ label: "Фантастический детектив", value: "115" },
{ label: "Фанфик", value: "113" },
{ label: "Фанфикшн", value: "464" },
{ label: "Фармацевт", value: "728" },
{ label: "Фарминг", value: "458" },
{ label: "Феи", value: "316" },
{ label: "Фелляция", value: "631" },
{ label: "Фемслэш", value: "127" },
{ label: "Фениксы", value: "454" },
{ label: "Фетиш груди", value: "559" },
{ label: "Философия", value: "208" },
{ label: "Фильмы", value: "479" },
{ label: "Флэшбэки", value: "585" },
{ label: "Фобии", value: "220" },
{ label: "Фольклор", value: "529" },
{ label: "Футанари", value: "619" },
{ label: "Футуристический сеттинг", value: "460" },
{ label: "Фэнтези мир", value: "244" },
{ label: "Хакеры", value: "475" },
{ label: "Харизматический герой", value: "467" },
{ label: "Хикикомори/Затворники", value: "524" },
{ label: "Хитроумный главный герой", value: "224" },
{ label: "Хозяин подземелий", value: "608" },
{ label: "Холодный главный герой", value: "354" },
{ label: "Хорошие отношения с семьей", value: "566" },
{ label: "Хранители могил", value: "191" },
{ label: "Целители", value: "465" },
{ label: "Цзянши", value: "740" },
{ label: "Цундэрэ", value: "510" },
{ label: "Чаты", value: "629" },
{ label: "Человеческое оружие", value: "580" },
{ label: "Честный главный герой", value: "341" },
{ label: "Читы", value: "339" },
{ label: "Шантаж", value: "584" },
{ label: "Шеф-повар", value: "340" },
{ label: "Шикигами", value: "598" },
{ label: "Школа только для девочек", value: "668" },
{ label: "Шота", value: "573" },
{ label: "Шоу-бизнес", value: "481" },
{ label: "Шпионский детектив", value: "118" },
{ label: "Шпионы", value: "614" },
{ label: "Эволюция", value: "304" },
{ label: "Эгоистичный главный герой", value: "594" },
{ label: "Эйдетическая память", value: "468" },
{ label: "Экзорсизм", value: "564" },
{ label: "Экономика", value: "553" },
{ label: "Эксгибиционизм", value: "673" },
{ label: "Эксперименты с людьми", value: "248" },
{ label: "Элементальная магия", value: "471" },
{ label: "Эльфы", value: "284" },
{ label: "Эмоционально слабый гг", value: "770" },
{ label: "Эпизодический", value: "649" },
{ label: "Эпическое фэнтези", value: "109" },
{ label: "Эротическая фантастика", value: "129" },
{ label: "Эротический фанфик", value: "128" },
{ label: "Эротическое фэнтези", value: "130" },
{ label: "Юмористическая фантастика", value: "143" },
{ label: "Юмористическое фэнтези", value: "108" },
{ label: "Юный любовный интерес", value: "511" },
{ label: "Яды", value: "484" },
{ label: "Языкастые персонажи", value: "480" },
{ label: "Языковой барьер", value: "182" },
{ label: "Яндере", value: "314" },
{ label: "Японские силы самообороны", value: "610" },
{ label: "Ярко выраженная романтика", value: "365" },
{ label: "R-15 Японское возрастное огр.", value: "332" },
{ label: "R-18", value: "424" },
],
inputType: FilterInputs.Checkbox,
},
];
-936
View File
@@ -1,936 +0,0 @@
const dayjs = require("dayjs");
const fetchApi = require("@libs/fetchApi");
const fetchFile = require("@libs/fetchFile");
const languages = require("@libs/languages");
const Status = require("@libs/novelStatus");
const FilterInputs = require("@libs/filterInputs");
const pluginId = "ReN";
const sourceName = "Renovels";
const baseUrl = "https://renovels.org";
async function popularNovels(page, { showLatestNovels, filters }) {
let url = baseUrl + "/api/search/catalog/?count=30&ordering=";
url += filters?.order ? filters?.order?.replace("+", "") : "-";
url += showLatestNovels ? "chapter_date" : filters?.sort || "rating";
if (filters?.type?.length) {
url += filters.type.map((i) => `&types=${i}`).join("");
}
if (filters?.statuss?.length) {
url += filters?.statuss.map((i) => `&status=${i}`).join("");
}
if (filters?.genres?.length) {
url += filters.genres.map((i) => `&genres=${i}`).join("");
}
if (filters?.categories?.length) {
url += filters.categories.map((i) => `&categories=${i}`).join("");
}
url += "&page=" + page;
const result = await fetchApi(url);
let body = await result.json();
let novels = [];
body.content.forEach((novel) =>
novels.push({
name: novel.rus_name,
cover: baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
url: baseUrl + "/novel/" + novel.dir,
})
);
return novels;
}
async function parseNovelAndChapters(novelUrl) {
let novelID = novelUrl.split("/")[4];
const result = await fetchApi(baseUrl + "/api/titles/" + novelID);
let body = await result.json();
let novel = {
url: novelUrl,
name: body.content.rus_name,
summary: body.content.description,
cover:
baseUrl +
(body.content.img?.high || body.content.img?.mid || body.content.img.low),
status:
body.content.status.name === "Продолжается"
? Status.Ongoing
: Status.Completed,
};
let tags = []
.concat(body.content.genres, body.content.categories, [])
.map((item) => item.name);
if (tags.length > 0) {
novel.genres = tags.join(", ");
}
let all = (body.content.count_chapters / 100 + 1) ^ 0;
let chapters = [];
for (let i = 0; i < all; i++) {
let chapterResult = await fetchApi(
baseUrl +
"/api/titles/chapters/?branch_id=" +
body.content.branches[0].id +
"&count=100&page=" +
(i + 1)
);
let volumes = await chapterResult.json();
volumes.content.forEach((chapter) => {
if (!chapter.is_paid || chapter.is_bought) {
chapters.push({
name: `Том ${chapter.tome} Глава ${chapter.chapter} ${chapter.name}`?.trim(),
releaseTime: dayjs(chapter.upload_date).format("LLL"),
url: `${baseUrl}/novel/${novelID}/${chapter.id}/`,
});
}
});
}
novel.chapters = chapters.reverse();
return novel;
}
async function parseChapter(chapterUrl) {
let url = baseUrl + "/api/titles/chapters/" + chapterUrl.split("/")[5];
const result = await fetchApi(url);
const body = await result.json();
return body.content.content;
}
async function searchNovels(searchTerm) {
const url = `${baseUrl}/api/search/?query=${searchTerm}&count=100&field=titles`;
const result = await fetchApi(url);
let body = await result.json();
let novels = [];
body.content.forEach((novel) =>
novels.push({
name: novel.rus_name,
cover: baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
url: baseUrl + "/novel/" + novel.dir,
})
);
return novels;
}
const filters = [
{
key: "sort",
label: "Сортировка",
values: [
{ label: "Рейтинг", value: "rating" },
{ label: "Просмотры", value: "views" },
{ label: "Лайкам", value: "votes" },
{ label: "Дате добавления", value: "id" },
{ label: "Дате обновления", value: "chapter_date" },
{ label: "Количество глав", value: "count_chapters" },
],
inputType: FilterInputs.Picker,
},
{
key: "order",
label: "Порядок",
values: [
{ label: "По убыванию", value: "-" }, // desc
{ label: "По возрастанию", value: "+" }, // asc
],
inputType: FilterInputs.Picker,
},
{
key: "type",
label: "Тип",
values: [
{ label: "Авторское", value: "1" },
{ label: "Другое", value: "7" },
{ label: "Запад", value: "5" },
{ label: "Китай", value: "4" },
{ label: "Корея", value: "3" },
{ label: "Фанфики", value: "6" },
{ label: "Япония", value: "2" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "statuss",
label: "Статус тайтла",
values: [
{ label: "Закончен", value: "0" },
{ label: "Продолжается", value: "1" },
{ label: "Заморожен", value: "2" },
{ label: "Нет переводчика", value: "3" },
{ label: "Анонс", value: "4" },
{ label: "Лицензировано", value: "5" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "genres",
label: "Жанры",
values: [
{ label: "Боевик", value: "112" },
{ label: "Война", value: "123" },
{ label: "Детектив", value: "114" },
{ label: "Драма", value: "125" },
{ label: "Историческая проза", value: "115" },
{ label: "ЛитРПГ", value: "109" },
{ label: "Любовные романы", value: "116" },
{ label: "Мистика", value: "103" },
{ label: "Научная фантастика", value: "121" },
{ label: "Повседневность", value: "128" },
{ label: "Подростковая проза", value: "113" },
{ label: "Политический роман", value: "119" },
{ label: "Попаданцы", value: "108" },
{ label: "Поэзия", value: "105" },
{ label: "Разное", value: "111" },
{ label: "РеалРПГ", value: "117" },
{ label: "Романтика", value: "126" },
{ label: "Современная проза", value: "102" },
{ label: "Современная фантастика", value: "127" },
{ label: "Спорт", value: "122" },
{ label: "Трагедия", value: "129" },
{ label: "Триллер", value: "110" },
{ label: "Триллер и саспенс", value: "124" },
{ label: "Ужасы", value: "106" },
{ label: "Фантастика", value: "101" },
{ label: "Фанфик", value: "107" },
{ label: "Фьюжн роман", value: "120" },
{ label: "Фэнтези", value: "100" },
{ label: "Эротика", value: "118" },
{ label: "Юмор", value: "104" },
],
inputType: FilterInputs.Checkbox,
},
{
key: "categories",
label: "Категории",
values: [
{ label: "[Награжденная работа]", value: "648" },
{ label: "18+", value: "423" },
{ label: "Абьюзеры", value: "663" },
{ label: "Авантюристы", value: "436" },
{ label: "Автоматоны", value: "593" },
{ label: "Агрессивные персонажи", value: "502" },
{ label: "Ад", value: "569" },
{ label: "Адаптация в радиопостановку", value: "581" },
{ label: "Академия", value: "153" },
{ label: "Актеры озвучки", value: "627" },
{ label: "Активный главный герой", value: "251" },
{ label: "Алхимия", value: "233" },
{ label: "Альтернативная история", value: "147" },
{ label: "Альтернативный мир", value: "156" },
{ label: "Амнезия/Потеря памяти", value: "344" },
{ label: "Анабиоз", value: "685" },
{ label: "Ангелы", value: "183" },
{ label: "Андрогинные персонажи", value: "321" },
{ label: "Андроиды", value: "203" },
{ label: "Анти-магия", value: "533" },
{ label: "Антигерой", value: "432" },
{ label: "Антикварный магазин", value: "623" },
{ label: "Антисоциальный главный герой", value: "613" },
{ label: "Антиутопия", value: "142" },
{ label: "Апатичный протагонист", value: "157" },
{ label: "Апокалипсис", value: "402" },
{ label: "Аранжированный брак", value: "375" },
{ label: "Армия", value: "639" },
{ label: "Артефакты", value: "234" },
{ label: "Артисты", value: "523" },
{ label: "Банды", value: "630" },
{ label: "БДСМ", value: "699" },
{ label: "Бедный главный герой", value: "398" },
{ label: "Безжалостный главный герой", value: "261" },
{ label: "Беззаботный главный герой", value: "438" },
{ label: "Безусловная любовь", value: "679" },
{ label: "Беременность", value: "250" },
{ label: "Бесполый главный герой", value: "325" },
{ label: "Бессмертные", value: "368" },
{ label: "Бесстрашный протагонист", value: "656" },
{ label: "Бесстыдный главный герой", value: "352" },
{ label: "Бесчестный главный герой", value: "716" },
{ label: "Библиотека", value: "428" },
{ label: "Бизнес-литература", value: "121" },
{ label: "Бизнесмен", value: "769" },
{ label: "Биочип", value: "238" },
{ label: "Бисексуальный главный герой", value: "773" },
{ label: "Близнецы", value: "263" },
{ label: "Боги", value: "317" },
{ label: "Богини", value: "439" },
{ label: "Боевая академия", value: "450" },
{ label: "Боевая фантастика", value: "149" },
{ label: "Боевое фэнтези", value: "104" },
{ label: "Боевые духи", value: "433" },
{ label: "Боевые соревнования", value: "493" },
{ label: "Божественная защита", value: "421" },
{ label: "Божественные силы", value: "327" },
{ label: "Борьба за власть", value: "599" },
{ label: "Брак", value: "446" },
{ label: "Брак по расчету", value: "188" },
{ label: "Братский комплекс", value: "159" },
{ label: "Братство", value: "487" },
{ label: "Братья и сестры", value: "577" },
{ label: "Буддизм", value: "745" },
{ label: "Быстрая культивация", value: "366" },
{ label: "Быстрообучаемый", value: "324" },
{ label: "Валькирии", value: "692" },
{ label: "Вампиры", value: "360" },
{ label: "Ваншот", value: "702" },
{ label: "Ведьмы", value: "281" },
{ label: "Вежливый главный герой", value: "379" },
{ label: "Верные подчиненные", value: "328" },
{ label: "Взрослый главный герой", value: "295" },
{ label: "Видит то, чего не видят другие", value: "671" },
{ label: "Виртуальная реальность", value: "401" },
{ label: "Владелец магазина", value: "681" },
{ label: "Внезапная сила", value: "455" },
{ label: "Внезапное богатство", value: "765" },
{ label: "Внешний вид отличается от факт", value: "419" },
{ label: "Военные Летописи", value: "743" },
{ label: "Возвращение из другого мира", value: "696" },
{ label: "Войны", value: "181" },
{ label: "Вокалоид", value: "701" },
{ label: "Волшебники/Волшебницы", value: "539" },
{ label: "Волшебные звери", value: "308" },
{ label: "Воображаемый друг", value: "651" },
{ label: "Воры", value: "414" },
{ label: "Воскрешение", value: "199" },
{ label: "Враги становятся возлюбленными", value: "498" },
{ label: "Враги становятся союзниками", value: "562" },
{ label: "Врата в другой мир", value: "609" },
{ label: "Врачи", value: "376" },
{ label: "Временной парадокс", value: "275" },
{ label: "Всемогущий главный герой", value: "169" },
{ label: "Вторжение на землю", value: "198" },
{ label: "Второй шанс", value: "231" },
{ label: "Вуайеризм", value: "790" },
{ label: "Выживание", value: "380" },
{ label: "Высокомерные персонажи", value: "361" },
{ label: "Гадание", value: "595" },
{ label: "Гарем рабов", value: "777" },
{ label: "Гг - женщина", value: "186" },
{ label: "Гг - мужчина", value: "166" },
{ label: "Гг силен с самого начала", value: "171" },
{ label: "Геймеры", value: "392" },
{ label: "Генералы", value: "326" },
{ label: "Генетические модификации", value: "657" },
{ label: "Гениальный главный герой", value: "617" },
{ label: "Герои", value: "285" },
{ label: "Героиня — сорванец", value: "583" },
{ label: "Героическая фантастика", value: "140" },
{ label: "Героическое фэнтези", value: "107" },
{ label: "Герой влюбляется первым", value: "187" },
{ label: "Гетерохромия", value: "570" },
{ label: "Гильдии", value: "411" },
{ label: "Гипнотизм", value: "755" },
{ label: "Главный герой — бог", value: "547" },
{ label: "Главный герой — гуманоид", value: "636" },
{ label: "Главный герой — наполовину чел", value: "445" },
{ label: "Главный герой — отец", value: "792" },
{ label: "Главный герой — раб", value: "780" },
{ label: "Главный герой — ребенок", value: "488" },
{ label: "Главный герой — рубака", value: "476" },
{ label: "Главный герой влюбляется первы", value: "683" },
{ label: "Главный герой играет роль", value: "472" },
{ label: "Главный герой носит очки", value: "672" },
{ label: "Главный герой пацифист", value: "698" },
{ label: "Гладиаторы", value: "603" },
{ label: "Глуповатый главный герой", value: "385" },
{ label: "Гоблины", value: "586" },
{ label: "Големы", value: "620" },
{ label: "Гомункул", value: "789" },
{ label: "Горничные", value: "459" },
{ label: "Городское фэнтези", value: "105" },
{ label: "Госпиталь", value: "800" },
{ label: "Готовка", value: "302" },
{ label: "Гриндинг", value: "393" },
{ label: "Дао Компаньон", value: "462" },
{ label: "Даосизм", value: "760" },
{ label: "Дварфы", value: "323" },
{ label: "Двойная личность", value: "642" },
{ label: "Двойник", value: "601" },
{ label: "Дворецкий", value: "660" },
{ label: "Дворяне", value: "168" },
{ label: "Дворянство/Аристократия", value: "437" },
{ label: "Девушки-монстры", value: "669" },
{ label: "Демоническая техника культивац", value: "722" },
{ label: "Демоны", value: "152" },
{ label: "Денежный долг", value: "783" },
{ label: "Депрессия", value: "555" },
{ label: "Детектив", value: "116" },
{ label: "Детективы", value: "612" },
{ label: "Детская литература", value: "123" },
{ label: "Дискриминация", value: "162" },
{ label: "Добыча денег в приоритете", value: "396" },
{ label: "Документальная проза", value: "122" },
{ label: "Долгая разлука", value: "307" },
{ label: "Домашние дела", value: "290" },
{ label: "Домогательство", value: "470" },
{ label: "Драконы", value: "303" },
{ label: "Драконьи всадники", value: "607" },
{ label: "Древние времена", value: "221" },
{ label: "Древний Китай", value: "374" },
{ label: "Дружба", value: "217" },
{ label: "Друзья детства", value: "282" },
{ label: "Друзья становятся врагами", value: "567" },
{ label: "Друиды", value: "497" },
{ label: "Дух лисы", value: "784" },
{ label: "Духи/призраки", value: "172" },
{ label: "Духовный советник", value: "435" },
{ label: "Душевность", value: "633" },
{ label: "Души", value: "254" },
{ label: "Европейская атмосфера", value: "521" },
{ label: "Ёкаи", value: "575" },
{ label: "Есть аниме-адаптация", value: "154" },
{ label: "Есть видеоигра по мотивам", value: "552" },
{ label: "Есть манга", value: "155" },
{ label: "Есть манхва-адаптация", value: "517" },
{ label: "Есть маньхуа-адаптация", value: "388" },
{ label: "Есть сериал-адаптация", value: "492" },
{ label: "Есть фильм", value: "173" },
{ label: "Женища-наставник", value: "505" },
{ label: "Жесткая, двуличная личность", value: "346" },
{ label: "Жестокие персонажи", value: "503" },
{ label: "Жестокое обращение с ребенком", value: "654" },
{ label: "Жестокость", value: "245" },
{ label: "Животноводство", value: "754" },
{ label: "Животные черты", value: "528" },
{ label: "Жизнь в квартире", value: "606" },
{ label: "Жрицы", value: "615" },
{ label: "Заботливый главный герой", value: "288" },
{ label: "Забывчивый главный герой", value: "628" },
{ label: "Заговоры", value: "289" },
{ label: "Закалка тела", value: "362" },
{ label: "Законники", value: "756" },
{ label: "Замкнутый главный герой", value: "589" },
{ label: "Запечатанная сила", value: "430" },
{ label: "Застенчивые персонажи", value: "508" },
{ label: "Звери", value: "237" },
{ label: "Звери-компаньоны", value: "301" },
{ label: "Злой протагонист", value: "243" },
{ label: "Злые боги", value: "504" },
{ label: "Злые организации", value: "563" },
{ label: "Злые религии", value: "734" },
{ label: "Знаменитости", value: "473" },
{ label: "Знаменитый главный герой", value: "531" },
{ label: "Знания современного мира", value: "297" },
{ label: "Зомби", value: "409" },
{ label: "Игра на выживание", value: "274" },
{ label: "Игривый протагонист", value: "717" },
{ label: "Игровая система рейтинга", value: "391" },
{ label: "Игровые элементы", value: "266" },
{ label: "Игрушки (18+)", value: "676" },
{ label: "Из грязи в князи", value: "416" },
{ label: "Из женщины в мужчину", value: "709" },
{ label: "Из мужчины в женщину", value: "715" },
{ label: "Из полного в худого", value: "767" },
{ label: "Из слабого в сильного", value: "202" },
{ label: "Извращенный главный герой", value: "434" },
{ label: "Изгои", value: "534" },
{ label: "Изменение расы", value: "664" },
{ label: "Изменения внешнего вида", value: "300" },
{ label: "Изменения личности", value: "219" },
{ label: "Изнасилование", value: "229" },
{ label: "Изображения жестокости", value: "355" },
{ label: "ИИ", value: "235" },
{ label: "Империи", value: "748" },
{ label: "Инвалидность", value: "684" },
{ label: "Индустриализация", value: "292" },
{ label: "Инженер", value: "163" },
{ label: "Инцест", value: "525" },
{ label: "Искусственный интеллект", value: "236" },
{ label: "Исследования", value: "670" },
{ label: "Историческая проза", value: "119" },
{ label: "Исторические приключения", value: "120" },
{ label: "Исторический детектив", value: "117" },
{ label: "Исторический любовный роман", value: "138" },
{ label: "Историческое фэнтези", value: "110" },
{ label: "Каннибализм", value: "456" },
{ label: "Карточные игры", value: "682" },
{ label: "Киберпанк", value: "141" },
{ label: "Киберспорт", value: "686" },
{ label: "Кланы", value: "796" },
{ label: "Класс безработного", value: "735" },
{ label: "Клоны", value: "447" },
{ label: "Клубы", value: "216" },
{ label: "Книги", value: "427" },
{ label: "Книги навыков", value: "400" },
{ label: "Книжный червь", value: "519" },
{ label: "Коварство", value: "230" },
{ label: "Коллеги", value: "704" },
{ label: "Колледж/Университет", value: "726" },
{ label: "Кома", value: "776" },
{ label: "Командная работа", value: "496" },
{ label: "Комедийный оттенок", value: "582" },
{ label: "Комплекс неполноценности", value: "550" },
{ label: "Комплекс семейных отношений", value: "223" },
{ label: "Конкуренция", value: "746" },
{ label: "Контроль разума/сознания", value: "357" },
{ label: "Копейщик", value: "425" },
{ label: "Королевская битва", value: "797" },
{ label: "Королевская власть", value: "178" },
{ label: "Королевства", value: "259" },
{ label: "Короткий любовный роман", value: "136" },
{ label: "Коррупция", value: "457" },
{ label: "Космическая фантастика", value: "146" },
{ label: "Космические войны", value: "697" },
{ label: "Красивый герой", value: "226" },
{ label: "Крафт", value: "377" },
{ label: "Кризис личности", value: "640" },
{ label: "Кругосветное путешествие", value: "353" },
{ label: "Кудере", value: "506" },
{ label: "Кузены", value: "718" },
{ label: "Кузнец", value: "518" },
{ label: "Кукловоды", value: "501" },
{ label: "Куклы/марионетки", value: "624" },
{ label: "Культивация", value: "241" },
{ label: "Куннилингус", value: "667" },
{ label: "Легенды", value: "500" },
{ label: "Легкая жизнь", value: "644" },
{ label: "Ленивый главный герой", value: "621" },
{ label: "Лидерство", value: "494" },
{ label: "Лоли", value: "213" },
{ label: "Лотерея", value: "477" },
{ label: "Любовное фэнтези", value: "135" },
{ label: "Любовный треугольник", value: "218" },
{ label: "Любовь детства", value: "560" },
{ label: "Любовь с первого взгляда", value: "737" },
{ label: "Магические надписи", value: "453" },
{ label: "Магические печати", value: "369" },
{ label: "Магические технологии", value: "440" },
{ label: "Магия", value: "165" },
{ label: "Магия призыва", value: "418" },
{ label: "Мазохистские персонажи", value: "688" },
{ label: "Манипулятивные персонажи", value: "249" },
{ label: "Мания", value: "212" },
{ label: "Мастер на все руки", value: "466" },
{ label: "Мастурбация", value: "788" },
{ label: "Махо-сёдзё", value: "556" },
{ label: "Медицинские знания", value: "507" },
{ label: "Медленная романтическая линия", value: "232" },
{ label: "Медленное развитие на старте", value: "694" },
{ label: "Межпространственные путешестви", value: "404" },
{ label: "Менеджмент", value: "294" },
{ label: "Мертвый главный герой", value: "761" },
{ label: "Месть", value: "209" },
{ label: "Метаморфы", value: "335" },
{ label: "Меч и магия", value: "180" },
{ label: "Мечник", value: "647" },
{ label: "Мечты", value: "738" },
{ label: "Милая история", value: "725" },
{ label: "Милое дитя", value: "637" },
{ label: "Милый главный герой", value: "714" },
{ label: "Мировое дерево", value: "463" },
{ label: "Мистика", value: "151" },
{ label: "Мистический ореол вокруг семьи", value: "483" },
{ label: "Мифические звери", value: "491" },
{ label: "Мифология", value: "530" },
{ label: "Младшие братья", value: "785" },
{ label: "Младшие сестры", value: "527" },
{ label: "ММОРПГ (ЛитРПГ)", value: "395" },
{ label: "Множество перемещенных людей", value: "549" },
{ label: "Множество реальностей", value: "370" },
{ label: "Множество реинкарнированных лю", value: "330" },
{ label: "Модели", value: "678" },
{ label: "Молчаливый персонаж", value: "721" },
{ label: "Монстры", value: "192" },
{ label: "Мужская гей-пара", value: "706" },
{ label: "Мужчина-яндере", value: "267" },
{ label: "Музыка", value: "635" },
{ label: "Музыкальные группы", value: "634" },
{ label: "Мутации", value: "693" },
{ label: "Мутированные существа", value: "405" },
{ label: "Навык кражи", value: "662" },
{ label: "Навязчивая любовь", value: "206" },
{ label: "Наемники", value: "412" },
{ label: "Назойливый возлюбленный", value: "561" },
{ label: "Наивный главный герой", value: "189" },
{ label: "Наркотики", value: "689" },
{ label: "Нарциссический главный герой", value: "532" },
{ label: "Насилие сексуального характера", value: "774" },
{ label: "Наследование", value: "452" },
{ label: "Научная фантастика", value: "144" },
{ label: "Национализм", value: "406" },
{ label: "Не блещущий внешне главный гер", value: "415" },
{ label: "Не родные братья и сестры", value: "526" },
{ label: "Небеса", value: "568" },
{ label: "Небесное испытание", value: "367" },
{ label: "Негуманоидный главный герой", value: "659" },
{ label: "Недоверчивый главный герой", value: "546" },
{ label: "Недооцененный главный герой", value: "258" },
{ label: "Недоразумения", value: "309" },
{ label: "Неизлечимая болезнь", value: "196" },
{ label: "Некромант", value: "397" },
{ label: "Нелинейная история", value: "269" },
{ label: "Ненавистный главный герой", value: "548" },
{ label: "Ненадежный рассказчик", value: "277" },
{ label: "Нерезиденты", value: "772" },
{ label: "Нерешительный главный герой", value: "744" },
{ label: "Несерьезный главный герой", value: "384" },
{ label: "Несколько временных линий", value: "576" },
{ label: "Несколько гг", value: "268" },
{ label: "Несколько главных героев", value: "537" },
{ label: "Несколько идентичностей", value: "652" },
{ label: "Несколько личностей", value: "536" },
{ label: "Нетораре", value: "732" },
{ label: "Нетори", value: "514" },
{ label: "Неудачливый главный герой", value: "618" },
{ label: "Ниндзя", value: "441" },
{ label: "Новелла", value: "103" },
{ label: "Обещание из детства", value: "742" },
{ label: "Обманщик", value: "485" },
{ label: "Обмен телами", value: "214" },
{ label: "Обнаженка", value: "490" },
{ label: "Обольщение", value: "731" },
{ label: "Оборотни", value: "661" },
{ label: "Обратный гарем", value: "351" },
{ label: "Общество монстров", value: "329" },
{ label: "Обязательство", value: "602" },
{ label: "Огнестрельное оружие", value: "291" },
{ label: "Ограниченное время жизни", value: "194" },
{ label: "Одержимость", value: "512" },
{ label: "Одинокий главный герой", value: "394" },
{ label: "Одиночество", value: "306" },
{ label: "Одиночное проживание", value: "313" },
{ label: "Околосмертные переживания", value: "759" },
{ label: "Оммёдзи", value: "597" },
{ label: "Омоложение", value: "338" },
{ label: "Организованная преступность", value: "540" },
{ label: "Оргия", value: "775" },
{ label: "Орки", value: "331" },
{ label: "Освоение навыков", value: "336" },
{ label: "Основано на аниме", value: "605" },
{ label: "Основано на видео игре", value: "720" },
{ label: "Основано на визуальной новелле", value: "793" },
{ label: "Основано на песне", value: "700" },
{ label: "Основано на фильме", value: "604" },
{ label: "Осторожный главный герой", value: "240" },
{ label: "Отаку", value: "571" },
{ label: "Открытый космос", value: "264" },
{ label: "Отношения в сети", value: "727" },
{ label: "Отношения между людьми и нелюд", value: "311" },
{ label: "Отношения на расстоянии", value: "791" },
{ label: "Отношения Сенпай-Коухай", value: "572" },
{ label: "Отношения ученика и учителя", value: "515" },
{ label: "Отношения учитель-ученик", value: "429" },
{ label: "Отношения хозяин-слуга", value: "312" },
{ label: "Отомэ игра", value: "733" },
{ label: "Отсутствие здравого смысла", value: "645" },
{ label: "Отсутствие родителей", value: "625" },
{ label: "Офисный роман", value: "707" },
{ label: "Официанты", value: "703" },
{ label: "Охотники", value: "378" },
{ label: "Очаровательный главный герой", value: "687" },
{ label: "Падшее дворянство", value: "655" },
{ label: "Падшие ангелы", value: "565" },
{ label: "Пайзури", value: "665" },
{ label: "Паразиты", value: "591" },
{ label: "Параллельные миры", value: "207" },
{ label: "Парк", value: "265" },
{ label: "Парк развлечений", value: "632" },
{ label: "Пародия", value: "407" },
{ label: "Певцы/Певицы", value: "739" },
{ label: "Первая любовь", value: "729" },
{ label: "Первоисточник новеллы — манга", value: "611" },
{ label: "Первый раз", value: "787" },
{ label: "Перемещение в игровой мир", value: "588" },
{ label: "Перемещение в иной мир", value: "750" },
{ label: "Перерождение в ином мире", value: "751" },
{ label: "Переселение души/трансмиграция", value: "257" },
{ label: "Персонаж использует щит", value: "666" },
{ label: "Петля времени", value: "200" },
{ label: "Пираты", value: "771" },
{ label: "Писатели", value: "482" },
{ label: "Питомцы", value: "349" },
{ label: "Племенное общество", value: "381" },
{ label: "Повелитель демонов", value: "283" },
{ label: "Подземелья", value: "322" },
{ label: "Подростковая проза", value: "114" },
{ label: "Пожелания", value: "278" },
{ label: "Познание Дао", value: "443" },
{ label: "Покинутое дитя", value: "590" },
{ label: "Полигамия", value: "386" },
{ label: "Политика", value: "170" },
{ label: "Политический роман", value: "133" },
{ label: "Полиция", value: "764" },
{ label: "Полулюди", value: "420" },
{ label: "Популярный любовный интерес", value: "342" },
{ label: "Постапокалипсис", value: "145" },
{ label: "Постапокалиптика", value: "184" },
{ label: "Потерянные цивилизации", value: "749" },
{ label: "Похищения людей", value: "723" },
{ label: "Поэзия", value: "111" },
{ label: "Правонарушители", value: "713" },
{ label: "Прагматичный главный герой", value: "592" },
{ label: "Преданный любовный интерес", value: "225" },
{ label: "Предательство", value: "222" },
{ label: "Предвидение", value: "177" },
{ label: "Прекрасная героиня", value: "158" },
{ label: "Преступники", value: "736" },
{ label: "Преступность", value: "204" },
{ label: "Призванный герой", value: "262" },
{ label: "Призраки", value: "176" },
{ label: "Принуждение к отношениям", value: "643" },
{ label: "Принцессы", value: "795" },
{ label: "Притворная пара", value: "641" },
{ label: "Причудливые персонажи", value: "387" },
{ label: "Пришельцы/инопланетяне", value: "197" },
{ label: "Программист", value: "691" },
{ label: "Проклятия", value: "174" },
{ label: "Промывание мозгов", value: "578" },
{ label: "Пропуск времени", value: "256" },
{ label: "Пророчества", value: "499" },
{ label: "Проститутки", value: "786" },
{ label: "Прошлое играет большую роль", value: "320" },
{ label: "Прыжки между мирами", value: "741" },
{ label: "Психические силы", value: "359" },
{ label: "Психопаты", value: "270" },
{ label: "Публицистика", value: "124" },
{ label: "Путешествие во времени", value: "201" },
{ label: "Пытка", value: "280" },
{ label: "Рабы", value: "778" },
{ label: "Развитие личности", value: "125" },
{ label: "Развод", value: "758" },
{ label: "Разумные предметы", value: "286" },
{ label: "Расизм", value: "408" },
{ label: "Рассказ", value: "185" },
{ label: "Расторжения помолвки", value: "343" },
{ label: "Расы зооморфов", value: "315" },
{ label: "Реализм", value: "246" },
{ label: "Ревность", value: "730" },
{ label: "Редакторы", value: "705" },
{ label: "Реинкарнация", value: "372" },
{ label: "Реинкарнация в монстра", value: "310" },
{ label: "Реинкарнация в объект", value: "711" },
{ label: "Религии", value: "616" },
{ label: "Репортеры", value: "781" },
{ label: "Ресторан", value: "680" },
{ label: "Решительный главный герой", value: "242" },
{ label: "Робкий главный герой", value: "763" },
{ label: "Родитель одиночка", value: "708" },
{ label: "Родительский комплекс", value: "587" },
{ label: "Родословная", value: "239" },
{ label: "Романтическая эротика", value: "132" },
{ label: "Романтический подсюжет", value: "271" },
{ label: "Рост персонажа", value: "215" },
{ label: "Рыцари", value: "260" },
{ label: "Садистские персонажи", value: "674" },
{ label: "Самоотверженный главный герой", value: "747" },
{ label: "Самурай", value: "677" },
{ label: "Сборник коротких историй", value: "520" },
{ label: "Связанные сюжетные линии", value: "535" },
{ label: "Святые", value: "752" },
{ label: "Священники", value: "413" },
{ label: "Сдержанный главный герой", value: "762" },
{ label: "Секретные организации", value: "417" },
{ label: "Секреты", value: "626" },
{ label: "Секс рабы", value: "779" },
{ label: "Семейный конфликт", value: "757" },
{ label: "Семь добродетелей", value: "334" },
{ label: "Семь смертных грехов", value: "253" },
{ label: "Семья", value: "347" },
{ label: "Сёнэн-ай подсюжет", value: "690" },
{ label: "Серийные убийцы", value: "557" },
{ label: "Сестринский комплекс", value: "558" },
{ label: "Сила духа", value: "373" },
{ label: "Сила, требующая платы за польз", value: "579" },
{ label: "Сильная пара", value: "228" },
{ label: "Сильный в сильнейшего", value: "442" },
{ label: "Сильный любовный интерес", value: "273" },
{ label: "Синдром восьмиклассника", value: "211" },
{ label: "Сироты", value: "319" },
{ label: "Система уровней", value: "305" },
{ label: "Системный администратор", value: "538" },
{ label: "Сказка", value: "126" },
{ label: "Скрытие истинной личности", value: "451" },
{ label: "Скрытие истинных способностей", value: "348" },
{ label: "Скрытный главный герой", value: "252" },
{ label: "Скрытые способности", value: "247" },
{ label: "Скульпторы", value: "448" },
{ label: "Слабо выраженная романтическая", value: "318" },
{ label: "Слабый главный герой", value: "298" },
{ label: "Слепой главный герой", value: "794" },
{ label: "Слуги", value: "333" },
{ label: "Слэш", value: "131" },
{ label: "Смерть", value: "175" },
{ label: "Смерть близких", value: "444" },
{ label: "Собственнические персонажи", value: "227" },
{ label: "Современная проза", value: "150" },
{ label: "Современность", value: "167" },
{ label: "Современный любовный роман", value: "137" },
{ label: "Сожительство", value: "544" },
{ label: "Создание армии", value: "287" },
{ label: "Создание артефактов", value: "382" },
{ label: "Создание клана", value: "513" },
{ label: "Создание королевства", value: "293" },
{ label: "Создание навыков", value: "337" },
{ label: "Создание секты", value: "469" },
{ label: "Солдаты/военные", value: "193" },
{ label: "Сон", value: "622" },
{ label: "Состоятельные персонажи", value: "190" },
{ label: "Социальная иерархия по силе", value: "255" },
{ label: "Социальная фантастика", value: "148" },
{ label: "Социальные изгои", value: "542" },
{ label: "Спасение мира", value: "638" },
{ label: "Специальные способности", value: "179" },
{ label: "Спокойный главный герой", value: "160" },
{ label: "Справедливый главный герой", value: "719" },
{ label: "Средневековье", value: "296" },
{ label: "Ссорящаяся пара", value: "383" },
{ label: "Сталкеры", value: "710" },
{ label: "Старение", value: "299" },
{ label: "Стимпанк", value: "139" },
{ label: "Стоические персонажи", value: "509" },
{ label: "Стокгольмский синдром", value: "675" },
{ label: "Стратег", value: "495" },
{ label: "Стратегические битвы", value: "272" },
{ label: "Стратегия", value: "798" },
{ label: "Стрелки", value: "522" },
{ label: "Стрельба из лука", value: "461" },
{ label: "Студенческий совет", value: "551" },
{ label: "Судьба", value: "364" },
{ label: "Суккубы", value: "545" },
{ label: "Супер герои", value: "799" },
{ label: "Суровая подготовка", value: "356" },
{ label: "Таинственная болезнь", value: "195" },
{ label: "Таинственное прошлое", value: "358" },
{ label: "Тайная личность", value: "399" },
{ label: "Тайные отношения", value: "768" },
{ label: "Танцоры", value: "782" },
{ label: "Телохранители", value: "516" },
{ label: "Темное фэнтези", value: "106" },
{ label: "Тентакли", value: "712" },
{ label: "Террористы", value: "574" },
{ label: "Технологический разрыв", value: "658" },
{ label: "Тихие персонажи", value: "600" },
{ label: "Толстый главный герой", value: "389" },
{ label: "Торговцы", value: "489" },
{ label: "Травля/буллинг", value: "210" },
{ label: "Травник", value: "724" },
{ label: "Трагическое прошлое", value: "276" },
{ label: "Трансплантация воспоминаний", value: "449" },
{ label: "Триллер", value: "134" },
{ label: "Трудолюбивый главный герой", value: "164" },
{ label: "Тюрьма", value: "541" },
{ label: "Убийства", value: "205" },
{ label: "Убийцы", value: "345" },
{ label: "Убийцы драконов", value: "646" },
{ label: "Уверенный главный герой", value: "363" },
{ label: "Удачливый главный герой", value: "478" },
{ label: "Ужасы", value: "112" },
{ label: "Укротитель монстров", value: "422" },
{ label: "Умения из прошлой жизни", value: "371" },
{ label: "Умная пара", value: "554" },
{ label: "Умный главный герой", value: "161" },
{ label: "Уникальная техника Культивации", value: "350" },
{ label: "Уникальное оружие", value: "426" },
{ label: "Управление бизнесом", value: "403" },
{ label: "Управление временем", value: "279" },
{ label: "Управление кровью", value: "753" },
{ label: "Упрямый главный герой", value: "695" },
{ label: "Уродливый главный герой", value: "766" },
{ label: "Ускоренный рост", value: "390" },
{ label: "Усыновленные дети", value: "543" },
{ label: "Усыновленный главный герой", value: "486" },
{ label: "Уход за детьми", value: "474" },
{ label: "Учителя", value: "431" },
{ label: "Фамильяры", value: "596" },
{ label: "Фанатизм", value: "650" },
{ label: "Фантастические существа", value: "410" },
{ label: "Фантастический детектив", value: "115" },
{ label: "Фанфик", value: "113" },
{ label: "Фанфикшн", value: "464" },
{ label: "Фармацевт", value: "728" },
{ label: "Фарминг", value: "458" },
{ label: "Феи", value: "316" },
{ label: "Фелляция", value: "631" },
{ label: "Фемслэш", value: "127" },
{ label: "Фениксы", value: "454" },
{ label: "Фетиш груди", value: "559" },
{ label: "Философия", value: "208" },
{ label: "Фильмы", value: "479" },
{ label: "Флэшбэки", value: "585" },
{ label: "Фобии", value: "220" },
{ label: "Фольклор", value: "529" },
{ label: "Футанари", value: "619" },
{ label: "Футуристический сеттинг", value: "460" },
{ label: "Фэнтези мир", value: "244" },
{ label: "Хакеры", value: "475" },
{ label: "Харизматический герой", value: "467" },
{ label: "Хикикомори/Затворники", value: "524" },
{ label: "Хитроумный главный герой", value: "224" },
{ label: "Хозяин подземелий", value: "608" },
{ label: "Холодный главный герой", value: "354" },
{ label: "Хорошие отношения с семьей", value: "566" },
{ label: "Хранители могил", value: "191" },
{ label: "Целители", value: "465" },
{ label: "Цзянши", value: "740" },
{ label: "Цундэрэ", value: "510" },
{ label: "Чаты", value: "629" },
{ label: "Человеческое оружие", value: "580" },
{ label: "Честный главный герой", value: "341" },
{ label: "Читы", value: "339" },
{ label: "Шантаж", value: "584" },
{ label: "Шеф-повар", value: "340" },
{ label: "Шикигами", value: "598" },
{ label: "Школа только для девочек", value: "668" },
{ label: "Шота", value: "573" },
{ label: "Шоу-бизнес", value: "481" },
{ label: "Шпионский детектив", value: "118" },
{ label: "Шпионы", value: "614" },
{ label: "Эволюция", value: "304" },
{ label: "Эгоистичный главный герой", value: "594" },
{ label: "Эйдетическая память", value: "468" },
{ label: "Экзорсизм", value: "564" },
{ label: "Экономика", value: "553" },
{ label: "Эксгибиционизм", value: "673" },
{ label: "Эксперименты с людьми", value: "248" },
{ label: "Элементальная магия", value: "471" },
{ label: "Эльфы", value: "284" },
{ label: "Эмоционально слабый гг", value: "770" },
{ label: "Эпизодический", value: "649" },
{ label: "Эпическое фэнтези", value: "109" },
{ label: "Эротическая фантастика", value: "129" },
{ label: "Эротический фанфик", value: "128" },
{ label: "Эротическое фэнтези", value: "130" },
{ label: "Юмористическая фантастика", value: "143" },
{ label: "Юмористическое фэнтези", value: "108" },
{ label: "Юный любовный интерес", value: "511" },
{ label: "Яды", value: "484" },
{ label: "Языкастые персонажи", value: "480" },
{ label: "Языковой барьер", value: "182" },
{ label: "Яндере", value: "314" },
{ label: "Японские силы самообороны", value: "610" },
{ label: "Ярко выраженная романтика", value: "365" },
{ label: "R-15 Японское возрастное огр.", value: "332" },
{ label: "R-18", value: "424" },
],
inputType: FilterInputs.Checkbox,
},
];
module.exports = {
id: pluginId,
name: sourceName,
site: baseUrl,
version: "1.0.0",
lang: languages.Russian,
icon: "src/ru/renovels/icon.png",
filters,
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
fetchImage: fetchFile,
};
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "ruranobe";
export const name = "РуРанобэ";
export const icon = "";
export const version = "1.0.0";
export const site = "https://ruranobe.ru";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Автоматы","value":64},{"label":"Агрессивные персонажи","value":26},{"label":"Ад","value":338},{"label":"Адаптация манги","value":8},{"label":"Академия","value":5},{"label":"Алхимия","value":27},{"label":"Альтернативная реальность","value":30},{"label":"Амнезия","value":31},{"label":"Анабиоз","value":170},{"label":"Ангелы","value":38},{"label":"Андроиды","value":37},{"label":"Антигерой","value":43},{"label":"Антимагия","value":41},{"label":"Апатичный главный герой","value":46},{"label":"Апокалипсис","value":47},{"label":"Аристократия","value":51},{"label":"Артефакты","value":58},{"label":"Асоциальный главный герой","value":42},{"label":"R-15","value":570},{"label":"R-18","value":571}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "sferdrakon.novel";
export const name = "Уголок дракона";
export const icon = "";
export const version = "1.0.0";
export const site = "https://sferdrakon.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Главный герой — мужчина","value":419},{"label":"Демоны","value":193},{"label":"Драконы","value":218},{"label":"Есть аниме-адаптация","value":10},{"label":"Есть видеоигра","value":13},{"label":"Есть манга-адаптация","value":14},{"label":"Звероподобные","value":79},{"label":"Изнасилование","value":574},{"label":"Королевская особа","value":596},{"label":"Магия","value":412},{"label":"Магия призыва","value":694},{"label":"Наездники на драконах","value":216},{"label":"Рыцари","value":383},{"label":"Цундере","value":728},{"label":"Эльфы","value":235}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "si.novel";
export const name = "Сказочные истории";
export const icon = "";
export const version = "1.0.0";
export const site = "https://si.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Аристократия","value":51},{"label":"Война","value":750},{"label":"Главная героиня — женщина","value":277},{"label":"Драконы","value":218},{"label":"Древний Китай","value":34},{"label":"Жестокие персонажи","value":4},{"label":"Злобные благородные девы","value":743},{"label":"Комедийный подтекст","value":147},{"label":"Культивация","value":171},{"label":"Магия","value":412},{"label":"Отомэ-игры","value":504},{"label":"Перерождение в другом мире","value":580},{"label":"Перерождение в другом мире","value":720},{"label":"Повелитель демонов","value":191},{"label":"Попаданец в другой мир","value":723},{"label":"Смерть","value":185},{"label":"Средневековье","value":436},{"label":"Фэнтези","value":267},{"label":"Харизматичный протагонист","value":120},{"label":"Хитрый главный герой","value":173}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "snailulitka.novel";
export const name = "Snailulitka";
export const icon = "";
export const version = "1.0.0";
export const site = "https://snailulitka.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Алхимия","value":27},{"label":"Ассасины","value":61},{"label":"Бедный главный герой","value":541},{"label":"Богачи","value":753},{"label":"Виртуальная реальность","value":744},{"label":"Война","value":750},{"label":"Геймеры","value":301},{"label":"Героиня красавица","value":81},{"label":"Герой красавец","value":329},{"label":"Гильдии","value":324},{"label":"Главный герой - гений","value":308},{"label":"Главный герой — знаменитость","value":263},{"label":"Главный герой — мужчина","value":419},{"label":"Дворфы","value":224},{"label":"Драконы","value":218},{"label":"Есть маньхуа-адаптация","value":15},{"label":"Игровая рейтинговая система","value":300},{"label":"Книги навыков","value":654},{"label":"Лекари","value":334},{"label":"Месть","value":588}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "tsundoku.novel";
export const name = "TsundokuTrans";
export const icon = "";
export const version = "1.0.0";
export const site = "https://tsundoku.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Боги","value":318},{"label":"Боевая академия","value":75},{"label":"Вампиры","value":742},{"label":"Влияние прошлого","value":517},{"label":"Война","value":750},{"label":"Главная героиня — женщина","value":277},{"label":"Главный герой — мужчина","value":419},{"label":"Демоны","value":193},{"label":"Драконы","value":218},{"label":"Королевская особа","value":596},{"label":"Коррупция","value":157},{"label":"Магия","value":412},{"label":"Мастер подземелий","value":222},{"label":"Мечи и магия","value":697},{"label":"Мечники","value":698},{"label":"Монстры","value":454},{"label":"Недооцененный главный герой","value":734},{"label":"От слабого к сильному","value":752},{"label":"R-15","value":570},{"label":"R-18","value":571}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};
+219
View File
@@ -0,0 +1,219 @@
import { load as parseHTML } from "cheerio";
import dayjs from 'dayjs';
import { fetchFile, fetchApi } from "@libs/fetch";
import { Novel, Plugin, Chapter } from "@typings/plugin";
// import { parseMadaraDate } from "@libs/parseMadaraDate";
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
// import { showToast } from "@libs/showToast";
import { Filter, FilterInputs } from "@libs/filterInputs";
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from "@libs/defaultCover";
export const id = "ukr.novel";
export const name = "Переклади українською мовою";
export const icon = "";
export const version = "1.0.0";
export const site = "https://ukr.novel.tl";
export const filters: Filter[] = [{"key":"tags","label":"Тэги","values":[{"label":"Альтернативная реальность","value":30},{"label":"Апатичный главный герой","value":46},{"label":"Асоциальный главный герой","value":42},{"label":"Владелец уникального оружия","value":736},{"label":"Гарем рабов","value":656},{"label":"Герои","value":341},{"label":"Героиня красавица","value":81},{"label":"Герой использует щит","value":636},{"label":"Герой-трудяга","value":330},{"label":"Главный герой — мужчина","value":419},{"label":"Глухой к любви герой","value":194},{"label":"Демоны","value":193},{"label":"Друзья детства","value":128},{"label":"Есть аниме-адаптация","value":10},{"label":"Есть манга-адаптация","value":14},{"label":"Есть CD дорама-адаптация","value":12},{"label":"Зверолюди","value":39},{"label":"Магия","value":412},{"label":"Магия призыва","value":694},{"label":"От слабого к сильному","value":752}],"inputType":FilterInputs.Checkbox}];
exports["protected"] = false;
const baseUrl = site;
const domain = baseUrl.split('//')[1];
export const popularNovels: Plugin.popularNovels = async function (
page,
{ filters }
) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: filters?.tags?.length ?? 0 > 0 ? { tags: filters?.tags } : {},
hostname: domain,
limit: 40,
page,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = json.data?.projects?.content?.map((novel: any) => ({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}));
return novels;
};
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
async function (novelUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Book',
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus url covers{url}persons(langs:["ru","en","*"]roles:["author","illustrator","original_story","original_design"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{url shortName chapters{title publishDate url published}}}}}}}',
variables: {
hostname: domain,
project: novelUrl,
url: novelUrl,
},
}),
});
const json: any = await result.json();
const novel: Novel.instance = {
url: novelUrl,
name: json.data.project.title,
cover: json.data.project.covers[0]?.url
? baseUrl + json.data.project.covers[0].url
: defaultCover,
summary: parseHTML(json.data.project.annotation?.text).text(),
author:
json.data.project.persons[0].name.firstName +
' ' +
json.data.project.persons[0].name.lastName,
status:
json.data.project.translationStatus === 'active'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
let genres = []
.concat(json.data.project.tags, json.data.project.genres)
.map((item: any) => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres.length > 0) {
novel.genres = genres.join(',');
}
let novelChapters: Chapter.Item[] = [];
json.data.project.subprojects.content.forEach((work: any) =>
work.volumes.content.forEach((volume: any) =>
volume.chapters.forEach(
(chapter: any) =>
chapter?.published &&
novelChapters.push({
name: volume.shortName + ' ' + chapter.title,
url: novelUrl + '/' + volume.url + '/' + chapter.url,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
}),
),
),
);
novel.chapters = novelChapters;
return novel;
};
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'EReaderData',
query:
'query EReaderData($url:String!,$chapterSelector:Selector!){project(project:{fullUrl:$url}){title url}chapter(chapter:$chapterSelector){title text{text}}}',
variables: {
chapterSelector: {
fullUrl: chapterUrl,
},
url: chapterUrl,
},
}),
});
const json: any = await result.json();
return parseHTML(json.data.chapter.text.text).html();
};
export const searchNovels: Plugin.searchNovels = async (searchTerm) => {
const result = await fetchApi('https://api.novel.tl/api/site/v2/graphql', {
method: 'post',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
'Alt-Used': 'api.novel.tl',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
},
referrer: baseUrl,
body: JSON.stringify({
operationName: 'Projects',
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title url covers{url}}}}',
variables: {
filter: {
query: searchTerm,
},
hostname: domain,
limit: 40,
page: 1,
},
}),
});
const json: any = await result.json();
const novels: Novel.Item[] = [];
json?.data?.projects?.content?.forEach((novel: any) =>
novels.push({
name: novel.title,
url: baseUrl + '/r/' + novel.url,
cover: novel.covers[0]?.url
? baseUrl + novel.covers[0].url
: defaultCover,
}),
);
return novels;
};
export const fetchImage: Plugin.fetchImage = async (url) => {
return await fetchFile(url, {});
};