Files
lnreader-plugins/plugins/russian/ranobelib.ts
T
Rider21 bc17c5f576 Feat: resolveUrl, add neobook (#803)
* neobook

* add qs

* fix

* /

* fix

* expandURL -> resolveUrl

* fix Agitoon

* Update ranobelib.ts

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
2024-02-28 20:22:20 +07:00

526 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
const statusKey: { [key: number]: string } = {
1: NovelStatus.Ongoing,
2: NovelStatus.Completed,
3: NovelStatus.OnHiatus,
4: NovelStatus.Cancelled,
};
class RLIB implements Plugin.PluginBase {
id = "RLIB";
name = "RanobeLib";
site = "https://ranobelib.me";
version = "1.0.0";
icon = "src/ru/ranobelib/icon.png";
ui: string | undefined = undefined;
async popularNovels(
pageNo: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/manga-list?sort=";
url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate";
url += "&dir=" + (filters?.order?.value || "desc");
url += "&chapters[min]=" + (filters?.require_chapters?.value ? "1" : "0");
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
if (value instanceof Array && value.length) {
url += "&" + type + "[]=" + value.join("&" + type + "[]=");
}
if (value?.include instanceof Array && value.include.length) {
url += "&" + type + "[include][]=" +
value.include.join("&" + type + "[include][]=");
}
if (value?.exclude instanceof Array && value.exclude.length) {
url += "&" + type + "[exclude][]=" +
value.exclude.join("&" + type + "[exclude][]=");
}
});
url += "&page=" + pageNo;
const result = await fetchApi(url).then((res) => res.text());
const loadedCheerio = parseHTML(result);
this.ui = loadedCheerio("a.header-right-menu__item")
.attr("href")
?.replace?.(/[^0-9]/g, "");
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".media-card-wrap").each((index, element) => {
const name = loadedCheerio(element).find(".media-card__title").text();
const cover = loadedCheerio(element).find("a.media-card").attr("data-src");
const url = loadedCheerio(element).find("a.media-card").attr("href");
if (!url) return;
novels.push({ name, cover, path: url.replace(this.site, "") });
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".media-name__main").text()?.trim?.() || "",
};
novel.cover = loadedCheerio(".container_responsive img").attr("src");
novel.summary = loadedCheerio(".media-description__text").text().trim();
novel.genres = loadedCheerio('div[class="media-tags"]')
.text()
.trim()
.replace(/[\n\r]+/g, ", ")
.replace(/ /g, "");
loadedCheerio(
'div[class="media-info-list paper"] > [class="media-info-list__item"]',
).each(function () {
let name = loadedCheerio(this)
.find('div[class="media-info-list__title"]')
.text();
if (name === "Автор") {
novel.author = loadedCheerio(this)
.find('div[class="media-info-list__value"]')
.text()
.trim();
} else if (name === "Художник") {
novel.artist = loadedCheerio(this)
.find('div[class="media-info-list__value"]')
.text()
.trim();
}
});
this.ui = loadedCheerio("a.header-right-menu__item")
.attr("href")
?.replace?.(/[^0-9]/g, "");
const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/);
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]);
if (!novel.name) {
novel.name =
chaptersJson.manga.rusName ||
chaptersJson.manga.engName ||
chaptersJson.manga.name;
}
novel.status = statusKey[chaptersJson.manga.status] || NovelStatus.Unknown;
this.ui = chaptersJson?.user?.id;
if (!chaptersJson.chapters.list?.length) return novel;
const totalChapters = chaptersJson.chapters.list.length;
const customPage: { [key: number]: string } = {};
const customOrder: { [key: number]: number } = {};
if (chaptersJson.chapters.branches?.length && chaptersJson.chapters.branches.length > 1) {
//if the novel is being translated by more than one team
chaptersJson.chapters.branches.forEach(({ teams, id }) => {
if (teams?.length) {
customPage[id || 0] =
teams.find((team) => team.is_active)?.name || teams[0].name;
}
});
//fixes the chapter's position.
chaptersJson.chapters.list.forEach((chapter) => {
chapter.index = customOrder[chapter.branch_id || 0] || 1;
customOrder[chapter.branch_id || 0] =
(customOrder[chapter.branch_id || 0] || 1) + 1;
});
}
const chapters: Plugin.ChapterItem[] = [];
chaptersJson.chapters.list.forEach((chapter, chapterIndex) =>
chapters.push({
name:
"Том " + chapter.chapter_volume +
" Глава " + chapter.chapter_number +
(chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
path:
novelPath +
"/v" + chapter.chapter_volume +
"/c" + chapter.chapter_number +
"?bid=" + (chapter.branch_id || ""),
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
chapterNumber:
customOrder[chapter.branch_id || 0] - (chapter.index || 0) ||
totalChapters - chapterIndex,
page: customPage[chapter.branch_id || 0] || "Основной перевод",
}),
);
novel.chapters =
chaptersJson.chapters.branches?.length &&
chaptersJson.chapters.branches.length > 1
? chapters.sort((a, b) => {
if ((a.page || 0) > (b.page || 0)) return 1;
if ((a.page || 0) < (b.page || 0)) return -1;
return (a.chapterNumber || 0) - (b.chapterNumber || 0);
})
: chapters.reverse();
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const loadedCheerio = parseHTML(result);
loadedCheerio(".reader-container img").each((index, element) => {
const src =
loadedCheerio(element).attr("data-src") ||
loadedCheerio(element).attr("src");
if (!src?.startsWith("http")) {
loadedCheerio(element).attr("src", this.site + src);
} else {
loadedCheerio(element).attr("src", src);
}
loadedCheerio(element).removeAttr("data-src");
});
const chapterText = loadedCheerio(".reader-container").html();
return chapterText || "";
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(
this.site + "/search?q=" + searchTerm + "&type=manga",
);
const body = (await result.json()) as Manga[];
const novels: Plugin.NovelItem[] = [];
body.forEach((novel) =>
novels.push({
name: novel.rus_name || novel.name,
cover: novel.coverImage,
path: "/" + novel.slug,
}),
);
return novels;
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + path + (this.ui ? (isNovel ? "?" : "&") + "ui=" + this.ui : "");
filters = {
sort: {
label: "Сортировка",
value: "rate",
options: [
{ label: "Рейтинг", value: "rate" },
{ label: "Имя", value: "name" },
{ label: "Просмотры", value: "views" },
{ label: "Дате добавления", value: "created_at" },
{ label: "Дате обновления", value: "last_chapter_at" },
{ label: "Количество глав", value: "chap_count" },
],
type: FilterTypes.Picker,
},
order: {
label: "Порядок",
value: "desc",
options: [
{ label: "По убыванию", value: "desc" },
{ label: "По возрастанию", value: "asc" },
],
type: FilterTypes.Picker,
},
type: {
label: "Тип",
value: [],
options: [
{ label: "Авторский", value: "14" },
{ label: "Английский", value: "13" },
{ label: "Китай", value: "12" },
{ label: "Корея", value: "11" },
{ label: "Фанфик", value: "15" },
{ label: "Япония", value: "10" },
],
type: FilterTypes.CheckboxGroup,
},
format: {
label: "Формат выпуска",
value: { include: [], exclude: [] },
options: [
{ label: "4-кома (Ёнкома)", value: "1" },
{ label: "В цвете", value: "4" },
{ label: "Веб", value: "6" },
{ label: "Вебтун", value: "7" },
{ label: "Додзинси", value: "3" },
{ label: "Сборник", value: "2" },
{ label: "Сингл", value: "5" },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
status: {
label: "Статус перевода",
value: [],
options: [
{ label: "Продолжается", value: "1" },
{ label: "Завершен", value: "2" },
{ label: "Заморожен", value: "3" },
{ label: "Заброшен", value: "4" },
],
type: FilterTypes.CheckboxGroup,
},
manga_status: {
label: "Статус тайтла",
value: [],
options: [
{ label: "Онгоинг", value: "1" },
{ label: "Завершён", value: "2" },
{ label: "Анонс", value: "3" },
{ label: "Приостановлен", value: "4" },
{ label: "Выпуск прекращён", value: "5" },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: "Жанры",
value: { include: [], exclude: [] },
options: [
{ label: "Арт", value: "32" },
{ label: "Безумие", value: "91" },
{ label: "Боевик", value: "34" },
{ label: "Боевые искусства", value: "35" },
{ label: "Вампиры", value: "36" },
{ label: "Военное", value: "89" },
{ label: "Гарем", value: "37" },
{ label: "Гендерная интрига", value: "38" },
{ label: "Героическое фэнтези", value: "39" },
{ label: "Демоны", value: "81" },
{ label: "Детектив", value: "40" },
{ label: "Детское", value: "88" },
{ label: "Дзёсэй", value: "41" },
{ label: "Драма", value: "43" },
{ label: "Игра", value: "44" },
{ label: "Исекай", value: "79" },
{ label: "История", value: "45" },
{ label: "Киберпанк", value: "46" },
{ label: "Кодомо", value: "76" },
{ label: "Комедия", value: "47" },
{ label: "Космос", value: "83" },
{ label: "Магия", value: "85" },
{ label: "Махо-сёдзё", value: "48" },
{ label: "Машины", value: "90" },
{ label: "Меха", value: "49" },
{ label: "Мистика", value: "50" },
{ label: "Музыка", value: "80" },
{ label: "Научная фантастика", value: "51" },
{ label: "Омегаверс", value: "77" },
{ label: "Пародия", value: "86" },
{ label: "Повседневность", value: "52" },
{ label: "Полиция", value: "82" },
{ label: "Постапокалиптика", value: "53" },
{ label: "Приключения", value: "54" },
{ label: "Психология", value: "55" },
{ label: "Романтика", value: "56" },
{ label: "Самурайский боевик", value: "57" },
{ label: "Сверхъестественное", value: "58" },
{ label: "Сёдзё", value: "59" },
{ label: "Сёдзё-ай", value: "60" },
{ label: "Сёнэн", value: "61" },
{ label: "Сёнэн-ай", value: "62" },
{ label: "Спорт", value: "63" },
{ label: "Супер сила", value: "87" },
{ label: "Сэйнэн", value: "64" },
{ label: "Трагедия", value: "65" },
{ label: "Триллер", value: "66" },
{ label: "Ужасы", value: "67" },
{ label: "Фантастика", value: "68" },
{ label: "Фэнтези", value: "69" },
{ label: "Школа", value: "70" },
{ label: "Эротика", value: "71" },
{ label: "Этти", value: "72" },
{ label: "Юри", value: "73" },
{ label: "Яой", value: "74" },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
tags: {
label: "Теги",
value: { include: [], exclude: [] },
options: [
{ label: "Авантюристы", value: "328" },
{ label: "Антигерой", value: "176" },
{ label: "Бессмертные", value: "333" },
{ label: "Боги", value: "218" },
{ label: "Борьба за власть", value: "309" },
{ label: "Брат и сестра", value: "360" },
{ label: "Ведьма", value: "339" },
{ label: "Видеоигры", value: "204" },
{ label: "Виртуальная реальность", value: "214" },
{ label: "Владыка демонов", value: "349" },
{ label: "Военные", value: "198" },
{ label: "Воспоминания из другого мира", value: "310" },
{ label: "Выживание", value: "212" },
{ label: "ГГ женщина", value: "294" },
{ label: "ГГ имба", value: "292" },
{ label: "ГГ мужчина", value: "295" },
{ label: "ГГ не ояш", value: "325" },
{ label: "ГГ не человек", value: "331" },
{ label: "ГГ ояш", value: "326" },
{ label: "Главный герой бог", value: "324" },
{ label: "Глупый ГГ", value: "298" },
{ label: "Горничные", value: "171" },
{ label: "Гуро", value: "306" },
{ label: "Гяру", value: "197" },
{ label: "Демоны", value: "157" },
{ label: "Драконы", value: "313" },
{ label: "Древний мир", value: "317" },
{ label: "Зверолюди", value: "163" },
{ label: "Зомби", value: "155" },
{ label: "Исторические фигуры", value: "323" },
{ label: "Кулинария", value: "158" },
{ label: "Культивация", value: "161" },
{ label: "ЛГБТ", value: "344" },
{ label: "ЛитРПГ", value: "319" },
{ label: "Лоли", value: "206" },
{ label: "Магия", value: "170" },
{ label: "Машинный перевод", value: "345" },
{ label: "Медицина", value: "159" },
{ label: "Межгалактическая война", value: "330" },
{ label: "Монстр Девушки", value: "207" },
{ label: "Монстры", value: "208" },
{ label: "Мрачный мир", value: "316" },
{ label: "Музыка", value: "358" },
{ label: "Музыка", value: "209" },
{ label: "Ниндзя", value: "199" },
{ label: "Обратный Гарем", value: "210" },
{ label: "Офисные Работники", value: "200" },
{ label: "Пираты", value: "341" },
{ label: "Подземелья", value: "314" },
{ label: "Политика", value: "311" },
{ label: "Полиция", value: "201" },
{ label: "Преступники / Криминал", value: "205" },
{ label: "Призраки / Духи", value: "196" },
{ label: "Призыватели", value: "329" },
{ label: "Прыжки между мирами", value: "321" },
{ label: "Путешествие в другой мир", value: "318" },
{ label: "Путешествие во времени", value: "213" },
{ label: "Рабы", value: "355" },
{ label: "Ранги силы", value: "312" },
{ label: "Реинкарнация", value: "154" },
{ label: "Самураи", value: "202" },
{ label: "Скрытие личности", value: "315" },
{ label: "Средневековье", value: "174" },
{ label: "Традиционные игры", value: "203" },
{ label: "Умный ГГ", value: "303" },
{ label: "Характерный рост", value: "332" },
{ label: "Хикикомори", value: "167" },
{ label: "Эволюция", value: "322" },
{ label: "Элементы РПГ", value: "327" },
{ label: "Эльфы", value: "217" },
{ label: "Якудза", value: "165" },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
require_chapters: {
label: "Только проекты с главами",
value: true,
type: FilterTypes.Switch,
},
} satisfies Filters;
}
export default new RLIB();
interface responseBook {
hasStickyPermission: boolean;
bookmark?: null;
auth: boolean;
comments_version: string;
manga: Manga;
chapters: Chapters;
user?: User;
}
interface Manga {
id: number;
name: string;
rusName?: string;
rus_name?: string;
engName?: string;
slug: string;
status: number;
chapters_count: number;
altNames?: string[] | null;
coverImage?: string;
href?: string;
}
interface Chapters {
list?: ListEntity[];
teams?: TeamsEntity[];
branches?: BranchesEntity[];
is_paid?: string[] | null;
}
interface ListEntity {
index?: number; //crutch
chapter_id: number;
chapter_slug: string;
chapter_name: string;
chapter_number: string;
chapter_volume: number;
chapter_moderated: number;
chapter_user_id: number;
chapter_expired_at: string;
chapter_scanlator_id: number;
chapter_created_at: string;
status?: null;
price: number;
branch_id?: number;
username: string;
}
interface TeamsEntity {
name: string;
alt_name: string;
cover: string;
slug: string;
id: number;
branch_id: number;
sale: number;
href: string;
pivot: Pivot;
}
interface Pivot {
manga_id: number;
team_id: number;
}
interface BranchesEntity {
id: number;
manga_id: number;
name: string;
teams?: TeamsEntity1[] | null;
is_subscribed: boolean;
}
interface TeamsEntity1 {
id: number;
name: string;
slug: string;
cover: string;
branch_id: number;
is_active: number;
}
interface User {
id: string;
avatar: string;
access: boolean;
isAdmin: boolean;
paid: boolean;
}