Feat: resolveUrl, add neobook (#803)

* neobook

* add qs

* fix

* /

* fix

* expandURL -> resolveUrl

* fix Agitoon

* Update ranobelib.ts

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
This commit is contained in:
Rider21
2024-02-28 16:22:20 +03:00
committed by GitHub
parent 9b1417954f
commit bc17c5f576
24 changed files with 854 additions and 285 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters, FilterTypes } from "@libs/filterInputs";
import qs from "qs";
class NovelsOnline implements Plugin.PluginBase {
id = "NO.net";
@@ -115,7 +116,7 @@ class NovelsOnline implements Plugin.PluginBase {
"application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: "q=" + encodeURIComponent(searchTerm),
body: qs.stringify({ q: searchTerm }),
},
).then((res) => res.text());
+2 -1
View File
@@ -2,6 +2,7 @@ import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import qs from "qs";
class FreeWebNovel implements Plugin.PluginBase {
id = "FWN.com";
@@ -92,7 +93,7 @@ class FreeWebNovel implements Plugin.PluginBase {
Origin: this.site,
},
method: "POST",
body: "searchkey=" + encodeURIComponent(searchTerm),
body: qs.stringify({ searchkey: searchTerm }),
}).then((res) => res.text());
const loadedCheerio = parseHTML(body);
-186
View File
@@ -1,186 +0,0 @@
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
export const id = "agit.xyz";
export const name = "Agitoon";
export const site = "https://agit501.xyz/";
export const version = "1.0.0";
export const icon = "src/kr/agitoon/agit.png";
const baseUrl = site;
class Agitoon implements Plugin.PluginBase {
id = "agit.xyz";
name = "Agitoon";
icon = "src/kr/agitoon/agit.png";
site = "https://agit501.xyz/";
filters?: Filters | undefined;
version = "1.0.0";
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const list_limit = 20 * (pageNo - 1);
const day = new Date().getDay();
const res = await fetchApi(baseUrl + "novel/index.update.php", {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: `mode=get_data_novel_list_p&novel_menu=3&np_day=${day}&np_rank=1&np_distributor=0&np_genre=00&np_order=1&np_genre_ex_1=00&np_genre_ex_2=00&list_limit=${list_limit}&is_query_first=true`,
method: "POST",
});
const resJson = await res.json() as response;
const novels: Plugin.NovelItem[] = []
resJson?.list?.forEach((novel) => {
novels.push({
url: baseUrl + "novel/list/" + novel.wr_id,
name: novel.wr_subject,
cover: baseUrl + novel.np_dir + "/thumbnail/" + novel.np_thumbnail,
});
});
return novels;
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const novelId = novelUrl.split("/").reverse()[0];
// cheerio
const result = await fetchApi(novelUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body, { decodeEntities: false });
const name = loadedCheerio("h5.pt-2").text();
const summary = loadedCheerio(".pt-1.mt-1.pb-1.mb-1").text();
const author = loadedCheerio(".post-item-list-cate-v")
.first()
.text()
.split(" : ")
.reverse()[0];
const cover =
baseUrl.slice(0, baseUrl.length - 1) +
loadedCheerio("div.col-5.pr-0.pl-0 img").attr("src");
const genresTag = loadedCheerio(".col-7 > .post-item-list-cate > span");
let genres = "";
genresTag.each((_, element) => {
genres += loadedCheerio(element).text();
genres += ", ";
});
genres = genres.slice(0, genres.length - 2);
// normal REST HTTP requests
let chapters: Plugin.ChapterItem[] = [];
const res = await fetchApi(baseUrl + "novel/list.update.php", {
headers: {
"content-type":
"application/x-www-form-urlencoded; charset=UTF-8",
},
body: `mode=get_data_novel_list_c&wr_id_p=${novelId}&page_no=1&cnt_list=10000&order_type=Asc`,
method: "POST",
});
const resJson = await res.json() as responseBook
resJson?.list?.forEach((chapter) => {
chapters.push({
name: chapter.wr_subject,
url: baseUrl + `novel/view/${chapter.wr_id}/2`,
releaseTime: chapter.wr_datetime,
});
});
const novel: Plugin.SourceNovel = {
url: novelUrl,
name,
cover,
summary,
author,
status: "",
genres: genres,
chapters,
};
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const result = await fetchApi(chapterUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
let content = loadedCheerio("#id_wr_content").html() || '';
// gets rid of the popup thingy
content = content.replace(
"팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다",
""
);
return content;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const rawResults = await fetchApi("https://agit501.xyz/novel/search.php", {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: `mode=get_data_novel_list_p_sch&search_novel=${searchTerm}&list_limit=0`,
method: "POST",
});
const resJson = await rawResults.json() as response;
const novels: Plugin.NovelItem[] = []
resJson?.list?.forEach((novel) => {
novels.push({
url: baseUrl + "novel/list/" + novel.wr_id,
name: novel.wr_subject,
cover: baseUrl + novel.np_dir + "/thumbnail/" + novel.np_thumbnail,
});
});
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new Agitoon();
interface response {
list_limit: number;
list?: (ListEntity)[] | null;
list_count: number;
}
interface ListEntity {
wr_id: string;
wr_subject: string;
np_dir: string;
np_type_02: string;
np_thumbnail: string;
np_author: string;
wr_subject2: string;
wr_datetime: string;
np_distributor: string;
np_genre: string;
np_country: string;
np_age: string;
is_scrap: number;
}
interface responseBook {
list?: (ListEntity2)[] | null;
download_time: string;
}
interface ListEntity2 {
wr_id: string;
wr_subject: string;
wr_datetime: string;
url_view1: string;
novel_c_style1: string;
novel_c_str1: string;
data_novel_c_view: string;
}
+185
View File
@@ -0,0 +1,185 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { load as parseHTML } from "cheerio";
import qs from "qs";
class Agitoon implements Plugin.PluginBase {
id = "agit.xyz";
name = "Agitoon";
icon = "src/kr/agitoon/agit.png";
site = "https://agit501.xyz";
version = "1.0.0";
async popularNovels(
pageNo: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const res = await fetchApi(this.site + "/novel/index.update.php", {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: qs.stringify({
mode: "get_data_novel_list_p",
novel_menu: showLatestNovels ? "1" : "3",
np_day: new Date().getDay(),
np_rank: "1",
np_distributor: "0",
np_genre: "00",
np_order: "1",
np_genre_ex_1: "00",
np_genre_ex_2: "00",
list_limit: 20 * (pageNo - 1),
is_query_first: pageNo == 1,
}),
});
const resJson = (await res.json()) as response;
const novels: Plugin.NovelItem[] = [];
resJson?.list?.forEach((novel) =>
novels.push({
name: novel.wr_subject,
cover: this.site + novel.np_dir + "/thumbnail/" + novel.np_thumbnail,
path: novel.wr_id,
}),
);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const loadedCheerio = parseHTML(result, { decodeEntities: false });
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h5.pt-2").text(),
cover: this.site + loadedCheerio("div.col-5.pr-0.pl-0 img").attr("src"),
summary: loadedCheerio(".pt-1.mt-1.pb-1.mb-1").text(),
};
novel.author = loadedCheerio(".post-item-list-cate-v")
.first()
.text()
.split(" : ")[1];
const genres = loadedCheerio(".col-7 > .post-item-list-cate > span")
.map((index, element) => loadedCheerio(element).text().trim())
.get();
if (genres.length) {
novel.genres = genres.join(", ");
}
const chapters: Plugin.ChapterItem[] = [];
const res = await fetchApi(this.site + "/novel/list.update.php", {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: qs.stringify({
mode: "get_data_novel_list_c",
wr_id_p: novelPath,
page_no: "1",
cnt_list: "10000",
order_type: "Asc",
}),
});
const resJson = (await res.json()) as responseBook;
resJson?.list?.forEach((chapter) =>
chapters.push({
name: chapter.wr_subject,
path: chapter.wr_id + "/2",
releaseTime: chapter.wr_datetime,
}),
);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const loadedCheerio = parseHTML(result);
let content = loadedCheerio("#id_wr_content").html() || "";
// gets rid of the popup thingy
content = content
.replace("팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다", "")
.trim();
return content;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const rawResults = await fetchApi("https://agit501.xyz/novel/search.php", {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: qs.stringify({
mode: "get_data_novel_list_p_sch",
search_novel: searchTerm,
list_limit: 0,
}),
});
const resJson = (await rawResults.json()) as response;
const novels: Plugin.NovelItem[] = [];
resJson?.list?.forEach((novel) =>
novels.push({
name: novel.wr_subject,
cover: this.site + "/" + novel.np_dir + "/thumbnail/" + novel.np_thumbnail,
path: novel.wr_id,
}),
);
return novels;
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? "/novel/list/" : "/novel/view/") + path;
}
export default new Agitoon();
interface response {
list_limit: number;
list?: ListEntity[] | null;
list_count: number;
}
interface ListEntity {
wr_id: string;
wr_subject: string;
np_dir: string;
np_type_02: string;
np_thumbnail: string;
np_author: string;
wr_subject2: string;
wr_datetime: string;
np_distributor: string;
np_genre: string;
np_country: string;
np_age: string;
is_scrap: number;
}
interface responseBook {
list?: ListEntity2[] | null;
download_time: string;
}
interface ListEntity2 {
wr_id: string;
wr_subject: string;
wr_datetime: string;
url_view1: string;
novel_c_style1: string;
novel_c_str1: string;
data_novel_c_view: string;
}
+1 -1
View File
@@ -112,7 +112,7 @@ class freedlit implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("#bookListBlock > div").each((index, element) => {
loadedCheerio("#bookGridBlock > div").each((index, element) => {
const name = loadedCheerio(element).find("h4 > a").text()?.trim();
const cover = loadedCheerio(element).find("a > img").attr("src")?.trim();
const url = loadedCheerio(element).find("h4 > a").attr("href")?.trim();
+19 -16
View File
@@ -53,15 +53,14 @@ class AuthorToday implements Plugin.PluginBase {
cover: novel.coverUrl
? "https://cm.author.today/content/" + novel.coverUrl
: defaultCover,
path: "/work/" + novel.id,
path: novel.id.toString(),
}),
);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const workID = novelPath.split("/")[2];
async parseNovel(workID: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, {
headers: {
Authorization: token,
@@ -70,7 +69,7 @@ class AuthorToday implements Plugin.PluginBase {
const book = (await result.json()) as responseBook;
const novel: Plugin.SourceNovel = {
path: novelPath,
path: workID,
name: book.title,
cover: book.coverUrl ? book.coverUrl.split("?")[0] : defaultCover,
genres: book.tags?.join(", "),
@@ -105,7 +104,7 @@ class AuthorToday implements Plugin.PluginBase {
if (chapter.isAvailable && !chapter.isDraft) {
chapters.push({
name: chapter.title || "Глава " + (chapterIndex + 1),
path: `v1/work/${workID}/chapter/${chapter.id}/text`,
path: workID + "/" + chapter.id,
releaseTime: dayjs(
chapter.publishTime || chapter.lastModificationTime,
).format("LLL"),
@@ -119,11 +118,15 @@ class AuthorToday implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(apiUrl + chapterPath, {
headers: {
Authorization: token,
const [workID, chapterID] = chapterPath.split("/");
const result = await fetchApi(
apiUrl + `v1/work/${workID}/chapter/${chapterID}/text`,
{
headers: {
Authorization: token,
},
},
});
);
const json = (await result.json()) as encryptedСhapter;
if (json.code) {
@@ -160,26 +163,26 @@ class AuthorToday implements Plugin.PluginBase {
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("div.book-row").each((index, element) => {
loadedCheerio("a.work-row").each((index, element) => {
const name = loadedCheerio(element)
.find('div[class="book-title"] a')
.find('h4[class="work-title"]')
.text()
.trim();
let cover = loadedCheerio(element).find("img").attr("src");
const path = loadedCheerio(element)
.find('div[class="book-title"] a')
.attr("href");
let cover = loadedCheerio(element).find("img").attr("data-src");
const path = loadedCheerio(element).attr("href");
cover = cover ? cover.split("?")[0] : defaultCover;
if (!path) return;
novels.push({ name, cover, path });
novels.push({ name, cover, path: path.replace("/work/", "") });
});
return novels;
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
isNovel ? this.site + "/work/" + path : this.site + "/reader/" + path;
filters = {
sort: {
+7 -4
View File
@@ -39,7 +39,7 @@ class Bookriver implements Plugin.PluginBase {
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
path: "/book/" + novel.slug,
path: novel.slug,
}),
);
}
@@ -47,7 +47,8 @@ class Bookriver implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath).then((res) => res.text());
const result = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const loadedCheerio = parseHTML(result);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
@@ -72,7 +73,7 @@ class Bookriver implements Plugin.PluginBase {
if (chapter.available) {
chapters.push({
name: chapter.name,
path: "/reader/" + book?.slug + "/" + chapter.chapterId,
path: book?.slug + "/" + chapter.chapterId,
releaseTime: dayjs(
chapter.firstPublishedAt || chapter.createdAt || undefined,
).format("LLL"),
@@ -112,7 +113,7 @@ class Bookriver implements Plugin.PluginBase {
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
path: "/book/" + novel.slug,
path: novel.slug,
}),
);
@@ -120,6 +121,8 @@ class Bookriver implements Plugin.PluginBase {
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? "/book/" : "/reader/") + path;
filters = {
sort: {
+450
View File
@@ -0,0 +1,450 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
const statusKey: { [key: string]: string } = {
"0": NovelStatus.Unknown,
"1": NovelStatus.Ongoing,
"2": NovelStatus.Completed,
"3": NovelStatus.OnHiatus,
"4": NovelStatus.Cancelled,
};
class Neobook implements Plugin.PluginBase {
id = "neobook";
name = "Neobook";
site = "https://neobook.org";
version = "1.0.0";
icon = "src/ru/neobook/icon.png";
async fetchNovels(
page: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> {
const formData = new FormData();
formData.append("version", "4.0");
formData.append("uid", "0");
formData.append("utoken", "");
formData.append("resource", "general");
formData.append("action", "get_bundle");
formData.append("bundle", "bundle_books");
formData.append("target", "feed");
formData.append("page", page.toString());
formData.append("filter_category_id", filters?.category?.value || "0");
formData.append("filter_completed", "-1");
formData.append("filter_search", searchTerm || "");
formData.append("filter_tags", filters?.tags?.value || "");
formData.append(
"filter_sort",
showLatestNovels ? "new" : filters?.sort?.value || "popular",
);
formData.append("filter_timeread", filters?.timeread?.value || "0-999999");
const result = await fetchApi("https://api.neobook.org/", {
method: "post",
body: formData,
});
const json = (await result.json()) as { bundle_books: BundleBooks };
const novels: Plugin.NovelItem[] = [];
if (json.bundle_books.feed?.length) {
json.bundle_books.feed?.forEach((novel) =>
novels.push({
name: novel.title,
cover: novel?.attachment?.image?.m || defaultCover,
path: novel.token + "/",
}),
);
}
return novels;
}
popularNovels = this.fetchNovels;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const defaultOptions: any = {
filters: undefined,
showLatestNovels: false,
};
return this.fetchNovels(page, defaultOptions, searchTerm);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: "",
cover: defaultCover,
};
const bookRaw = body.match(/var postData = ({.*?});/);
if (bookRaw instanceof Array && bookRaw.length >= 2) {
const book = JSON.parse(bookRaw[1]) as Novels;
novel.name = book.title;
novel.summary = book.text?.replace?.(/<br>/g, "\n") || book.text_fix;
novel.author =
book.user && book.user.firstname && book.user.lastname
? book.user.firstname + " " + book.user.lastname
: book.user?.initials || "";
novel.status = statusKey[book.status || "0"] || NovelStatus.Unknown;
if (book.attachment?.image?.m) novel.cover = book.attachment.image.m;
if (book.tags?.length) novel.genres = book.tags.join(",");
const chapters: Plugin.ChapterItem[] = [];
book.chapters?.forEach((chapter, chapterIndex) => {
if (chapter.access == "1" && chapter.status == "1") {
chapters.push({
name: chapter.title || "Глава " + (chapterIndex + 1),
path: `?book=${book.token}&chapter=${chapter.token}`,
releaseTime: null,
chapterNumber: Number(chapter.sort) || chapterIndex + 1,
});
}
});
novel.chapters = chapters;
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const bookRaw = body.match(/var data = ({.*?});/);
let chapterText = "";
if (bookRaw instanceof Array && bookRaw.length >= 2) {
const book = JSON.parse(bookRaw[1]) as Novels;
const token = chapterPath.split("=")[2];
const chapter = book.chapters?.find?.(
(chapter) => chapter.token == token,
);
chapterText = (chapter?.data?.html || "").replace(/<br>/g, "");
}
return chapterText;
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? "/book/" : "/reader/") + path;
filters = {
sort: {
label: "Сортировка:",
value: "popular",
options: [
{ label: "Сначала популярные", value: "popular" },
{ label: "Сначала новые", value: "new" },
{ label: "В случайном порядке", value: "rand" },
],
type: FilterTypes.Picker,
},
timeread: {
label: "Время прочтения:",
value: "0-999999",
options: [
{ label: "Все", value: "0-999999" },
{ label: "Менее 15 минут", value: "0-900" },
{ label: "15-30 минут", value: "900-1800" },
{ label: "30-60 минут", value: "1800-3600" },
{ label: "1-2 часа", value: "3600-7200" },
{ label: "Более 2 часов", value: "7200-999999" },
],
type: FilterTypes.Picker,
},
category: {
label: "Жанр:",
value: "",
options: [
{ label: "Все", value: "" },
{ label: "Антиутопия", value: "10" },
{ label: "Детектив", value: "13" },
{ label: "Детские книги", value: "14" },
{ label: "Драма", value: "15" },
{ label: "Другое", value: "16" },
{ label: "История", value: "18" },
{ label: "Мелодрама", value: "21" },
{ label: "Мистика", value: "22" },
{ label: "Научная фантастика", value: "23" },
{ label: "Нон-фикшн", value: "35" },
{ label: "Подростки и молодежь", value: "26" },
{ label: "Постапокалипсис", value: "27" },
{ label: "Поэзия", value: "28" },
{ label: "Приключения", value: "29" },
{ label: "Рассказ", value: "34" },
{ label: "Роман", value: "36" },
{ label: "Творчество", value: "40" },
{ label: "Триллер", value: "91" },
{ label: "Ужасы", value: "90" },
{ label: "Фантастика", value: "41" },
{ label: "Фанфик", value: "42" },
{ label: "Фэнтези", value: "44" },
{ label: "Эротика", value: "46" },
{ label: "Юмор", value: "47" },
],
type: FilterTypes.Picker,
},
tags: {
label: "Тэги:",
value: "",
type: FilterTypes.TextInput,
},
} satisfies Filters;
}
export default new Neobook();
interface BundleBooks {
categories: any[];
topSection: any[];
feed: Novels[];
}
interface BottomSection {
id: string;
boost: string;
preferenceid: string;
type_id: string;
contentTypeid: string;
name: string;
title: string;
more: string;
part: string;
items: Novels[];
}
interface Novels {
status?: string;
error?: string;
login?: Login;
requestid?: string;
bundleBooks?: BundleBooks;
available?: string;
boosted?: string;
id?: string;
token: string;
type?: string;
typeid?: string;
verified?: string;
audio?: string;
labelid?: string;
labelTitle?: string;
languageid?: string;
languageTitle?: string;
categoryid?: string;
categoryTitle?: string;
price?: Price;
priceSpecial?: Price;
priceDiscount?: string;
title: string;
text: string;
text_fix: string;
copyright?: string;
align?: string;
completed?: string;
timereadS?: string;
views?: string;
likes?: string;
comments?: string;
bookmarks?: string;
isLike?: string;
isBookmark?: string;
isPurchase?: string;
isSelf?: string;
dateEdit?: string;
datePublish?: string;
link_path?: string;
user?: NovelsUser;
attachment?: Attachment;
adult?: string;
adultPlaceholder?: string;
nft?: string;
feed?: Feed;
counters?: NovelsCounters;
rating?: Rating;
readTime?: ReadTime;
readProgress?: ReadProgress;
tags?: string[];
chapters?: Chapter[];
lastComments?: any[];
bottomSection?: BottomSection;
carouselItems?: Novels[];
book?: Novels;
activeChapterIndex?: number;
}
interface Attachment {
has: string;
id: string;
typeid: string;
userid: string;
width: string;
height: string;
duration: string;
image: Image;
video: string;
origin: string;
ext: string;
}
interface Image {
s: string;
m: string;
l: string;
has?: string;
}
interface Chapter {
id: string;
token: string;
status: string;
sort: string;
free: string;
title: string;
words: string;
access: string;
loaded: string;
data?: Data;
}
interface Data {
available: string;
access: string;
id: string;
token: string;
postid: string;
free: string;
title: string;
html: string;
dateEdit: string;
datePublish: string;
attachment: Attachment;
lastComments: any[];
}
interface NovelsCounters {
impressions: Bookmarks;
views: Bookmarks;
likes: Bookmarks;
comments: Bookmarks;
bookmarks: Bookmarks;
}
interface Bookmarks {
value: string;
formatted: string;
}
interface Feed {
name: string;
canRemoved: string;
}
interface Login {
uid: string;
utoken: string;
has: string;
banned: string;
verified: string;
acc: string;
arm: string;
grm: string;
counters: LoginCounters;
user: LoginUser;
pro: any[];
earn: any[];
boost: any[];
deposit: any[];
affiliate: any[];
preferences: any[];
}
interface LoginCounters {
notifications: string;
messages: string;
}
interface LoginUser {
id: string;
username: string;
firstname: string;
lastname: string;
photo: string;
status: string;
balance: Balance;
connectedService: string;
}
interface Balance {
neo: Price;
}
interface Price {
has: string;
value: string;
currency: Currency;
formatted: string;
deadline: string;
}
type Currency = "NEO";
interface Rating {
value: string;
count: string;
my: string;
}
interface ReadProgress {
chapter: string;
progress: string;
}
interface ReadTime {
hours: string;
minutes: string;
}
interface NovelsUser {
available: string;
boosted: string;
id: string;
status: string;
verified: string;
metrica: string;
cover: Image;
photo: Image;
username: string;
firstname: string;
lastname: string;
initials: string;
link: string;
about: string;
subscriptions: string;
subscribers: string;
publications: string;
books: string;
poems: string;
posts: string;
drafts: string;
isPro: string;
isAdult: string;
isBanned: string;
isSubscription: string;
isBlock: string;
gr: string;
}
+5 -4
View File
@@ -27,7 +27,7 @@ class novelOvh implements Plugin.PluginBase {
novels.push({
name: novel.name.ru,
cover: novel.poster,
path: "/novel/" + novel.slug,
path: novel.slug,
}),
);
@@ -36,7 +36,7 @@ class novelOvh implements Plugin.PluginBase {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(
this.site + novelPath + "?_data=routes/reader/book/$slug/index",
this.resolveUrl(novelPath, true) + "?_data=routes/reader/book/$slug/index",
);
const json = (await result.json()) as responseNovel;
@@ -82,7 +82,7 @@ class novelOvh implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(
"https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[3],
"https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[1],
);
const book = (await result.json()) as responseChapter;
const image = Object.fromEntries(
@@ -106,7 +106,7 @@ class novelOvh implements Plugin.PluginBase {
novels.push({
name: novel.name.ru,
cover: novel.poster,
path: "/novel/" + novel.slug,
path: novel.slug,
}),
);
@@ -154,6 +154,7 @@ class novelOvh implements Plugin.PluginBase {
};
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path;
filters = {
sort: {
+1
View File
@@ -212,6 +212,7 @@ class TL implements Plugin.PluginBase {
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => "https://" + path;
filters = {
tags: {
+6 -5
View File
@@ -67,7 +67,7 @@ class RNBH implements Plugin.PluginBase {
novels.push({
name: novel.names.rus || novel.names.eng || novel.names.original,
cover: novel.poster.medium,
path: "/ranobe/" + novel.id,
path: novel.id.toString(),
}),
);
@@ -75,7 +75,7 @@ class RNBH implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + "/api" + novelPath);
const result = await fetchApi(this.site + "/api/ranobe/" + novelPath);
const json = (await result.json()) as { data: responseNovel };
const novel: Plugin.SourceNovel = {
@@ -98,7 +98,7 @@ class RNBH implements Plugin.PluginBase {
const chapters: Plugin.ChapterItem[] = [];
const chaptersRaw = await fetchApi(
this.site + "/api" + novelPath + "/contents",
this.site + "/api/ranobe/" + novelPath + "/contents",
);
const chaptersJSON = (await chaptersRaw.json()) as {
volumes: VolumesEntity[];
@@ -123,7 +123,7 @@ class RNBH implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const result = await fetchApi(this.resolveUrl(chapterPath));
const body = await result.text();
const loadedCheerio = parseHTML(body);
@@ -161,7 +161,7 @@ class RNBH implements Plugin.PluginBase {
novel.name ||
novel?.names?.original ||
"",
path: "/ranobe/" + novel.id,
path: novel.id.toString(),
cover: novel?.image?.replace("/small", "/medium"),
}),
);
@@ -170,6 +170,7 @@ class RNBH implements Plugin.PluginBase {
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => this.site + "/ranobe/" + path;
filters = {
sort: {
+13 -7
View File
@@ -67,7 +67,8 @@ class RLIB implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const body = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
@@ -104,6 +105,10 @@ class RLIB implements Plugin.PluginBase {
}
});
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]);
@@ -136,7 +141,7 @@ class RLIB implements Plugin.PluginBase {
customOrder[chapter.branch_id || 0] =
(customOrder[chapter.branch_id || 0] || 1) + 1;
});
};
}
const chapters: Plugin.ChapterItem[] = [];
chaptersJson.chapters.list.forEach((chapter, chapterIndex) =>
@@ -146,7 +151,7 @@ class RLIB implements Plugin.PluginBase {
" Глава " + chapter.chapter_number +
(chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
path:
"/" + chaptersJson.manga.slug +
novelPath +
"/v" + chapter.chapter_volume +
"/c" + chapter.chapter_number +
"?bid=" + (chapter.branch_id || ""),
@@ -164,16 +169,15 @@ class RLIB implements Plugin.PluginBase {
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.site + chapterPath + (this.ui ? "&ui=" + this.ui : ""),
).then((res) => res.text());
const result = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const loadedCheerio = parseHTML(result);
loadedCheerio(".reader-container img").each((index, element) => {
@@ -211,6 +215,8 @@ class RLIB implements Plugin.PluginBase {
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + path + (this.ui ? (isNovel ? "?" : "&") + "ui=" + this.ui : "");
filters = {
sort: {
+7 -7
View File
@@ -44,19 +44,18 @@ class ReN implements Plugin.PluginBase {
const novels: Plugin.NovelItem[] = body.content.map((novel) => ({
name: novel.main_name || novel.secondary_name,
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
path: "/novel/" + novel.dir,
path: novel.dir,
}));
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const novelID = novelPath.split("/")[2];
async parseNovel(novelID: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + "/api/titles/" + novelID);
const body = (await result.json()) as { content: responseNovel };
const novel: Plugin.SourceNovel = {
path: novelPath,
path: novelID,
name:
body.content.main_name ||
body.content.secondary_name ||
@@ -104,7 +103,7 @@ class ReN implements Plugin.PluginBase {
"Том " + chapter.tome +
" Глава " + chapter.chapter +
(chapter.name ? " " + chapter.name.trim() : ""),
path: "/novel/" + novelID + "/" + chapter.id,
path: novelID + "/" + chapter.id,
releaseTime: dayjs(chapter.upload_date).format("LLL"),
chapterNumber: chapter.index,
});
@@ -117,7 +116,7 @@ class ReN implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[3];
const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[1];
const result = await fetchApi(url);
const body = (await result.json()) as { content: responseСhapter };
@@ -137,7 +136,7 @@ class ReN implements Plugin.PluginBase {
novels.push({
name: novel.main_name || novel.secondary_name,
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
path: "/novel/" + novel.dir,
path: novel.dir,
}),
);
@@ -145,6 +144,7 @@ class ReN implements Plugin.PluginBase {
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path;
filters = {
sort: {