Plugins fixed (#903)

* test

* fast fix

* fix

* fix

* Update ranobelib.ts
This commit is contained in:
Rider21
2024-04-25 17:53:16 +03:00
committed by GitHub
parent 2c4490e5bd
commit 9bc558936d
24 changed files with 63683 additions and 62335 deletions
+55 -41
View File
@@ -2,14 +2,16 @@ 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 regex =
/<script id="__NEXT_DATA__" type="application\/json">(\{.*?\})<\/script>/;
class Bookriver implements Plugin.PluginBase {
id = 'bookriver';
name = 'Bookriver';
site = 'https://bookriver.ru';
version = '1.0.0';
version = '1.0.1';
icon = 'src/ru/bookriver/icon.png';
async popularNovels(
@@ -29,12 +31,12 @@ class Bookriver implements Plugin.PluginBase {
}
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
if (jsonRaw) {
const json: response = JSON.parse(jsonRaw);
const jsonRaw = result.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
const json: response = JSON.parse(jsonRaw[1]);
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach(novel =>
novels.push({
name: novel.name,
@@ -43,6 +45,7 @@ class Bookriver implements Plugin.PluginBase {
}),
);
}
return novels;
}
@@ -50,50 +53,60 @@ class Bookriver implements Plugin.PluginBase {
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result);
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
const book = json.props.pageProps.state.book?.bookPage;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: book?.name || '',
cover: book?.coverImages[0].url,
summary: book?.annotation,
author: book?.author?.name,
genres: book?.tags?.map(tag => tag.name).join(', '),
status:
book?.statusComplete === 'writing'
? NovelStatus.Ongoing
: NovelStatus.Completed,
name: '',
};
const chapters: Plugin.ChapterItem[] = [];
book?.ebook?.chapters?.forEach((chapter, chapterIndex) => {
if (chapter.available) {
chapters.push({
name: chapter.name,
path: book?.slug + '/' + chapter.chapterId,
releaseTime: dayjs(
chapter.firstPublishedAt || chapter.createdAt || undefined,
).format('LLL'),
chapterNumber: chapterIndex + 1,
const jsonRaw = result.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
const book: BooksEntity = JSON.parse(jsonRaw[1]).props.pageProps.state
.book.bookPage;
novel.name = book.name || '';
novel.cover = book.coverImages[0].url;
novel.summary = book.annotation;
novel.author = book.author?.name;
novel.status =
book?.statusComplete === 'writing'
? NovelStatus.Ongoing
: NovelStatus.Completed;
if (book.tags?.length)
novel.genres = book.tags?.map(tag => tag.name).join(',');
if (book?.ebook?.chapters?.length) {
const chapters: Plugin.ChapterItem[] = [];
book.ebook.chapters.forEach((chapter, chapterIndex) => {
if (chapter.available) {
chapters.push({
name: chapter.name,
path: novelPath + '/' + chapter.chapterId,
releaseTime: dayjs(
chapter.firstPublishedAt || chapter.createdAt || undefined,
).format('LLL'),
chapterNumber: chapterIndex + 1,
});
}
});
novel.chapters = chapters;
}
});
novel.chapters = chapters;
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const url = 'https://api.bookriver.ru/api/v1/books/chapter/text/';
const result = await fetchApi(url + chapterPath.split('/').pop());
const json = (await result.json()) as responseChapter;
const { data }: responseChapter = await fetchApi(
url + chapterPath.split('/').pop(),
).then(res => res.json());
let chapterText = json.data.content || 'Конец произведения';
if (json.data?.audio?.available) {
chapterText += '\n' + json.data.audio.url;
let chapterText = data.content || 'Конец произведения';
if (data?.audio?.available) {
chapterText += '\n' + data.audio.url;
}
return chapterText;
@@ -109,11 +122,12 @@ class Bookriver implements Plugin.PluginBase {
'&page=' +
pageNo +
'&perPage=10';
const result = await fetchApi(url);
const json = (await result.json()) as responseSearch;
const { data }: responseSearch = await fetchApi(url).then(res =>
res.json(),
);
const novels: Plugin.NovelItem[] = [];
json.data?.books?.forEach(novel =>
data?.books?.forEach(novel =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
+10 -7
View File
@@ -10,7 +10,7 @@ class ficbook implements Plugin.PluginBase {
id = 'ficbook';
name = 'ficbook';
site = 'https://ficbook.net';
version = '1.0.0';
version = '1.0.1';
icon = 'src/ru/ficbook/icon.png';
async popularNovels(
@@ -149,14 +149,17 @@ class ficbook implements Plugin.PluginBase {
formData.append('term', searchTerm);
formData.append('page', pageNo.toString());
const result = await fetchApi(this.site + '/search/fanfic', {
method: 'POST',
body: formData,
});
const json = (await result.json()) as { data: Data };
const { data }: { data: Data } = await fetchApi(
this.site + '/search/fanfic',
{
method: 'POST',
body: formData,
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
json.data?.data?.forEach(novel => {
data?.data?.forEach(novel => {
const name = novel.title.trim();
const path = '/readfic/' + novel.slug;
const cover = novel.cover
+14 -12
View File
@@ -16,7 +16,7 @@ class Neobook implements Plugin.PluginBase {
id = 'neobook';
name = 'Neobook';
site = 'https://neobook.org';
version = '1.0.0';
version = '1.0.1';
icon = 'src/ru/neobook/icon.png';
async fetchNovels(
@@ -46,15 +46,17 @@ class Neobook implements Plugin.PluginBase {
);
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 { bundle_books }: { bundle_books: BundleBooks } = await fetchApi(
'https://api.neobook.org/',
{
method: 'post',
body: formData,
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (json.bundle_books.feed?.length) {
json.bundle_books.feed?.forEach(novel =>
if (bundle_books.feed?.length) {
bundle_books.feed?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.attachment?.image?.m || defaultCover,
@@ -91,8 +93,8 @@ class Neobook implements Plugin.PluginBase {
};
const bookRaw = body.match(/var postData = ({.*?});/);
if (bookRaw instanceof Array && bookRaw.length >= 2) {
const book = JSON.parse(bookRaw[1]) as Novels;
if (bookRaw instanceof Array && bookRaw[1]) {
const book: Novels = JSON.parse(bookRaw[1]);
novel.name = book.title;
novel.summary = book.text?.replace?.(/<br>/g, '\n') || book.text_fix;
@@ -131,9 +133,9 @@ class Neobook implements Plugin.PluginBase {
const bookRaw = body.match(/var data = ({.*?});/);
let chapterText = '';
if (bookRaw instanceof Array && bookRaw.length >= 2) {
const book = JSON.parse(bookRaw[1]) as Novels;
if (bookRaw instanceof Array && bookRaw[1]) {
const token = chapterPath.split('=')[2];
const book: Novels = JSON.parse(bookRaw[1]);
const chapter = book.chapters?.find?.(chapter => chapter.token == token);
chapterText = (chapter?.data?.html || '').replace(/<br>/g, '');
}
+26 -28
View File
@@ -8,7 +8,7 @@ class novelOvh implements Plugin.PluginBase {
id = 'novelovh';
name = 'НовелОВХ';
site = 'https://novel.ovh';
version = '1.0.0';
version = '1.0.1';
icon = 'src/ru/novelovh/icon.png';
async popularNovels(
@@ -23,11 +23,12 @@ class novelOvh implements Plugin.PluginBase {
: filters?.sort?.value || 'averageRating') +
',desc';
const result = await fetchApi(url + '&_data=routes/reader/book/index');
const body = (await result.json()) as { books: BooksEntity[] };
const { books }: { books: BooksEntity[] } = await fetchApi(
url + '&_data=routes/reader/book/index',
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
body.books.forEach(novel =>
books.forEach(novel =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
@@ -39,25 +40,22 @@ class novelOvh implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(
const { book, chapters }: responseNovel = await fetchApi(
this.resolveUrl(novelPath, true) +
'?_data=routes/reader/book/$slug/index',
);
const json = (await result.json()) as responseNovel;
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: json.book.name.ru,
cover: json.book.poster,
genres: json.book.labels?.map?.(label => label.name).join(','),
summary: json.book.description,
name: book.name.ru,
cover: book.poster,
genres: book.labels?.map?.(label => label.name).join(','),
summary: book.description,
status:
json.book.status == 'ONGOING'
? NovelStatus.Ongoing
: NovelStatus.Completed,
book.status == 'ONGOING' ? NovelStatus.Ongoing : NovelStatus.Completed,
};
json.book.relations?.forEach(person => {
book.relations?.forEach(person => {
switch (person.type) {
case 'AUTHOR':
novel.author = person.publisher.name;
@@ -66,33 +64,32 @@ class novelOvh implements Plugin.PluginBase {
}
});
const chapters: Plugin.ChapterItem[] = [];
const chaptersRes: Plugin.ChapterItem[] = [];
json.chapters.forEach((chapter, chapterIndex) =>
chapters.push({
chapters.forEach((chapter, chapterIndex) =>
chaptersRes.push({
name:
chapter.title ||
'Том ' +
(chapter.volume || 0) +
' ' +
(chapter.name ||
'Глава ' +
(chapter.number || json.chapters.length - chapterIndex)),
'Глава ' + (chapter.number || chapters.length - chapterIndex)),
path: novelPath + '/' + chapter.id + '/0',
releaseTime: dayjs(chapter.createdAt).format('LLL'),
chapterNumber: json.chapters.length - chapterIndex,
chapterNumber: chapters.length - chapterIndex,
}),
);
novel.chapters = chapters.reverse();
novel.chapters = chaptersRes.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(
const book: responseChapter = await fetchApi(
'https://api.novel.ovh/v2/chapters/' + chapterPath.split('/')[1],
);
const book = (await result.json()) as responseChapter;
).then(res => res.json());
const image = Object.fromEntries(
book?.pages?.map(({ id, image }) => [id, image]) || [],
);
@@ -106,11 +103,12 @@ class novelOvh implements Plugin.PluginBase {
page: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/novel?search=${searchTerm}&page=${page - 1}`;
const result = await fetchApi(url + '&_data=routes/reader/book/index');
const body = (await result.json()) as { books: BooksEntity[] };
const { books }: { books: BooksEntity[] } = await fetchApi(
url + '&_data=routes/reader/book/index',
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
body.books.forEach(novel =>
books.forEach(novel =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
+89 -107
View File
@@ -3,7 +3,6 @@ import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
const statusKey: { [key: string]: string } = {
@@ -17,40 +16,47 @@ class TL implements Plugin.PluginBase {
id = 'TL';
name = 'NovelTL';
site = 'https://novel.tl';
version = '1.0.0';
version = '1.0.1';
icon = 'src/ru/noveltl/icon.png';
async popularNovels(
async fetchNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.site + '/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',
},
Referer: this.site,
body: JSON.stringify({
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?.value || [],
genres: filters?.genres?.value || [],
},
hostname: 'novel.tl',
limit: 40,
page,
const { data }: response = await fetchApi(
this.site + '/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',
},
}),
});
Referer: this.site,
body: JSON.stringify({
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 || undefined,
tags: filters.tags?.value?.length
? filters.tags.value
: undefined,
genres: filters.genres?.value?.length
? filters.genres.value
: undefined,
},
hostname: 'novel.tl',
limit: 40,
page,
},
}),
},
).then(res => res.json());
const json = (await result.json()) as response;
const novels: Plugin.NovelItem[] = [];
json.data?.projects?.content?.forEach(novel =>
data?.projects?.content?.forEach(novel =>
novels.push({
name: novel.title,
path: novel.fullUrl,
@@ -63,37 +69,52 @@ class TL implements Plugin.PluginBase {
return novels;
}
popularNovels = this.fetchNovels;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const defaultOptions: any = {
filters: {},
};
return this.fetchNovels(page, defaultOptions, searchTerm);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + '/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',
},
Referer: this.site,
body: JSON.stringify({
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: novelPath,
const { data }: response = await fetchApi(
this.site + '/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',
},
}),
});
const json = (await result.json()) as response;
Referer: this.site,
body: JSON.stringify({
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: novelPath,
},
}),
},
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: json.data.project?.title || '',
cover: json.data.project?.covers?.[0]?.url
? this.site + json.data.project.covers[0].url
name: data.project?.title || '',
cover: data.project?.covers?.[0]?.url
? this.site + data.project.covers[0].url
: defaultCover,
summary: json.data.project?.annotation?.text,
summary: data.project?.annotation?.text,
status:
statusKey[json.data.project?.translationStatus || 'unknown'] ||
statusKey[data.project?.translationStatus || 'unknown'] ||
NovelStatus.Unknown,
};
const genres = [json.data.project?.tags, json.data.project?.genres]
const genres = [data.project?.tags, data.project?.genres]
.flat()
.map(item => item?.nameRu || item?.nameEng)
.filter(item => item);
@@ -102,7 +123,7 @@ class TL implements Plugin.PluginBase {
novel.genres = genres.join(', ');
}
json.data.project?.persons?.forEach(person => {
data.project?.persons?.forEach(person => {
if (person.role == 'author' && person.name.firstName) {
novel.author =
person.name.firstName + ' ' + (person.name?.lastName || '');
@@ -115,7 +136,7 @@ class TL implements Plugin.PluginBase {
const chapters: Plugin.ChapterItem[] = [];
json.data.project?.subprojects?.content?.forEach(work =>
data.project?.subprojects?.content?.forEach(work =>
work.volumes.content.forEach((volume, volumeIndex) =>
volume.chapters.forEach((chapter, chapterIndex) => {
if (chapter.published) {
@@ -139,7 +160,7 @@ class TL implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + '/api/site/v2/graphql', {
const { data } = await fetchApi(this.site + '/api/site/v2/graphql', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
@@ -154,63 +175,24 @@ class TL implements Plugin.PluginBase {
url: decodeURI(chapterPath),
},
}),
});
const json = (await result.json()) as response;
}).then(res => res.json());
const loadedCheerio = parseHTML(json.data.chapter?.text?.text || '');
loadedCheerio('p > a[href]').each((index, element) => {
let src = loadedCheerio(element).attr('href') || '';
if (!src.startsWith('http')) {
src = this.site + src;
}
loadedCheerio(element).find('picture').remove();
loadedCheerio(element).removeAttr('href');
loadedCheerio(`<img src="${src}">`).appendTo(element);
});
const chapterText = loadedCheerio.html();
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.site + '/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',
},
Referer: this.site,
body: JSON.stringify({
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,
const chapterText = data.chapter?.text?.text || '';
if (chapterText.includes('<img')) {
return chapterText.replace(
/src="(.*?)"|href="(.*?)"/g,
(match: string, src: string, href: string) => {
if (src && !src.startsWith('http')) {
return `src="${this.site + src}"`;
} else if (href && !href.startsWith('http')) {
return `href="${this.site + href}"`;
}
return match;
},
}),
});
const json = (await result.json()) as response;
const novels: Plugin.NovelItem[] = [];
);
}
json?.data?.projects?.content?.forEach(novel =>
novels.push({
name: novel.title,
path: novel.fullUrl,
cover: novel?.covers?.[0]?.url
? this.site + novel.covers[0].url
: defaultCover,
}),
);
return novels;
return chapterText;
}
fetchImage = fetchFile;
+37 -47
View File
@@ -2,7 +2,6 @@ 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 } = {
@@ -14,7 +13,7 @@ const statusKey: { [key: number]: string } = {
class RNBH implements Plugin.PluginBase {
id = 'RNBH.org';
name = 'RanobeHub';
version = '1.0.0';
version = '1.0.1';
site = 'https://ranobehub.org';
icon = 'src/ru/ranobehub/icon.png';
@@ -58,12 +57,12 @@ class RNBH implements Plugin.PluginBase {
url += '&tags:negative=' + excludeTags.join(',');
}
}
url += '&take=40';
const result = await fetchApi(url);
const body = (await result.json()) as { resource: responseNovels[] };
const { resource }: { resource: responseNovels[] } = await fetchApi(
url + '&take=40',
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
body.resource.forEach(novel =>
resource.forEach(novel =>
novels.push({
name: novel.names.rus || novel.names.eng || novel.names.original,
cover: novel.poster.medium,
@@ -75,19 +74,20 @@ class RNBH implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + '/api/ranobe/' + novelPath);
const json = (await result.json()) as { data: responseNovel };
const { data }: { data: responseNovel } = await fetchApi(
this.site + '/api/ranobe/' + novelPath,
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: json.data.names.rus || json.data.names.eng || '',
cover: json.data.posters.medium,
summary: json.data.description.trim(),
author: json.data?.authors?.[0]?.name_eng || '',
status: statusKey[json.data.status.id] || NovelStatus.Unknown,
name: data.names.rus || data.names.eng || '',
cover: data.posters.medium,
summary: data.description.trim(),
author: data?.authors?.[0]?.name_eng || '',
status: statusKey[data.status.id] || NovelStatus.Unknown,
};
const tags = [json.data.tags.events, json.data.tags.genres]
const tags = [data.tags.events, data.tags.genres]
.flat()
.map(tags => tags?.names?.rus || tags?.names?.eng || tags?.title)
.filter(tags => tags);
@@ -97,21 +97,15 @@ class RNBH implements Plugin.PluginBase {
}
const chapters: Plugin.ChapterItem[] = [];
const chaptersRaw = await fetchApi(
const chaptersJSON: { volumes: VolumesEntity[] } = await fetchApi(
this.site + '/api/ranobe/' + novelPath + '/contents',
);
const chaptersJSON = (await chaptersRaw.json()) as {
volumes: VolumesEntity[];
};
).then(res => res.json());
chaptersJSON.volumes.forEach(volume =>
volume.chapters?.forEach(chapter =>
chapters.push({
name: chapter.name,
path:
volume.num && chapter.num
? novelPath + '/' + volume.num + '/' + chapter.num
: chapter.url.replace(this.site, ''),
path: novelPath + '/' + volume.num + '/' + chapter.num,
releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format(
'LLL',
),
@@ -125,38 +119,34 @@ class RNBH implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.resolveUrl(chapterPath));
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio('.chapter-hoticons').remove();
loadedCheerio('div.text:nth-child(1) img').each((index, element) => {
if (!loadedCheerio(element).attr('src')?.startsWith('http')) {
const dataMediaId = loadedCheerio(element).attr('data-media-id');
loadedCheerio(element).attr(
'src',
this.site + '/api/media/' + dataMediaId,
);
}
});
let chapterText = loadedCheerio('div.text:nth-child(1)').html() || '';
// Remove script tags
chapterText = chapterText?.replace(
/<\s*script[^>]*>[\s\S]*?<\/script>/gim,
'',
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const indexA = body.indexOf('<div class="title-wrapper">');
const indexB = body.indexOf(
'<div class="ui text container" style>',
indexA,
);
const chapterText = body
.substring(indexA, indexB)
.replace(
/<img data-media-id="(.*?)".*?>/g,
`<img src="${this.site}/api/media/$1"/>`,
);
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/api/fulltext/global?query=${searchTerm}&take=10`;
const result = await fetchApi(url);
const data = (await result.json()) as responseSearch[];
const result: responseSearch[] = await fetchApi(url).then(res =>
res.json(),
);
const novels: Plugin.NovelItem[] = [];
data
result
?.find(item => item?.meta?.key === 'ranobe')
?.data?.forEach(novel =>
novels.push({
+127 -218
View File
@@ -3,20 +3,12 @@ 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';
name = 'RanobeLib (OLD)';
site = 'https://old.ranobelib.me/old/';
version = '1.1.0';
icon = 'src/ru/ranobelib/icon.png';
ui: string | undefined = undefined;
@@ -27,51 +19,71 @@ class RLIB implements Plugin.PluginBase {
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');
let url = this.site + 'manga-list?page=' + pageNo;
url +=
'&sort_by=' +
(showLatestNovels
? 'last_chapter_at'
: filters?.sort_by?.value || 'rating_score');
url += '&sort_type=' + (filters?.sort_type?.value || 'desc');
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
if (value instanceof Array && value.length) {
url += '&' + type + '[]=' + value.join('&' + type + '[]=');
}
if (value?.include instanceof Array && value.include.length) {
url +=
'&' +
type +
'[include][]=' +
value.include.join('&' + type + '[include][]=');
}
if (value?.exclude instanceof Array && value.exclude.length) {
url +=
'&' +
type +
'[exclude][]=' +
value.exclude.join('&' + type + '[exclude][]=');
}
});
if (filters?.require_chapters?.value) {
url += '&chapters[min]=1';
}
if (filters?.types?.value?.length) {
url += '&types[]=' + filters.types.value.join('&types[]=');
}
if (filters?.scanlateStatus?.value?.length) {
url +=
'&scanlateStatus[]=' +
filters.scanlateStatus.value.join('&scanlateStatus[]=');
}
if (filters?.manga_status?.value?.length) {
url +=
'&manga_status[]=' +
filters.manga_status.value.join('&manga_status[]=');
}
url += '&page=' + pageNo;
if (filters?.genres) {
if (filters.genres.value?.include?.length) {
url += '&genres[]=' + filters.genres.value.include.join('&genres[]=');
}
if (filters.genres.value?.exclude?.length) {
url +=
'&genres_exclude[]=' +
filters.genres.value.exclude.join('&genres_exclude[]=');
}
}
if (filters?.tags) {
if (filters.tags.value?.include?.length) {
url += '&tags[]=' + filters.tags.value.include.join('&tags[]=');
}
if (filters.tags.value?.exclude?.length) {
url +=
'&tags_exclude[]=' +
filters.tags.value.exclude.join('&tags_exclude[]=');
}
}
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
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, '') });
});
const novelsRaw = result.match(/window\.__CATALOG_ITEMS__ = (\[.*?\]);/);
if (novelsRaw instanceof Array && novelsRaw.length >= 2) {
const novelsJson: resNovels[] = JSON.parse(novelsRaw[1]);
novelsJson.forEach(novel => {
novels.push({
name: novel.rus_name,
cover: novel.cover.default,
path: novel.slug_url,
});
});
}
return novels;
}
@@ -97,7 +109,7 @@ class RLIB implements Plugin.PluginBase {
.replace(/ /g, '');
loadedCheerio(
'div[class="media-info-list paper"] > [class="media-info-list__item"]',
'div.media-info-list > div[class="media-info-list__item"]',
).each(function () {
let name = loadedCheerio(this)
.find('div[class="media-info-list__title"]')
@@ -120,77 +132,26 @@ class RLIB implements Plugin.PluginBase {
.attr('href')
?.replace?.(/[^0-9]/g, '');
const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/);
const chaptersRaw = body.match(/window\.__CHAPTERS__ = (\[.*?\]);/);
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]);
const chaptersJson: resChapters[] = 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;
});
}
if (!chaptersJson?.length) return novel;
const chapters: Plugin.ChapterItem[] = [];
chaptersJson.chapters.list.forEach((chapter, chapterIndex) =>
chaptersJson.forEach(chapter =>
chapters.push({
name:
'Том ' +
chapter.chapter_volume +
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] || 'Основной перевод',
chapter.number +
(chapter.name ? ' ' + chapter.name.trim() : ''),
path: novelPath + '/v' + chapter.volume + '/c' + chapter.number,
chapterNumber: chapter.index + 1,
}),
);
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();
novel.chapters = chapters;
}
return novel;
}
@@ -218,17 +179,15 @@ class RLIB implements Plugin.PluginBase {
}
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 result = await fetchApi(this.site + 'api/manga?q=' + searchTerm);
const { data }: { data: resNovels[] } = await result.json();
const novels: Plugin.NovelItem[] = [];
body.forEach(novel =>
data.forEach(novel =>
novels.push({
name: novel.rus_name || novel.name,
cover: novel.coverImage,
path: '/' + novel.slug,
cover: novel?.cover?.default || '',
path: novel.slug_url,
}),
);
@@ -237,23 +196,25 @@ class RLIB implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + path + (this.ui ? (isNovel ? '?' : '&') + 'ui=' + this.ui : '');
this.site + path + (this.ui ? '?ui=' + this.ui : '');
filters = {
sort: {
sort_by: {
label: 'Сортировка',
value: 'rate',
value: 'rating_score',
options: [
{ label: 'Рейтинг', value: 'rate' },
{ label: 'Имя', value: 'name' },
{ label: 'Просмотры', value: 'views' },
{ label: 'Дате добавления', value: 'created_at' },
{ label: 'По рейтингу', value: 'rate_avg' },
{ label: 'По популярности', value: 'rating_score' },
{ label: 'По просмотрам', value: 'views' },
{ label: 'Количеству глав', value: 'chap_count' },
{ label: 'Дате обновления', value: 'last_chapter_at' },
{ label: 'Количество глав', value: 'chap_count' },
{ label: 'Дате добавления', value: 'created_at' },
{ label: 'По названию (A-Z)', value: 'name' },
{ label: 'По названию (А-Я)', value: 'rus_name' },
],
type: FilterTypes.Picker,
},
order: {
sort_type: {
label: 'Порядок',
value: 'desc',
options: [
@@ -262,34 +223,20 @@ class RLIB implements Plugin.PluginBase {
],
type: FilterTypes.Picker,
},
type: {
types: {
label: 'Тип',
value: [],
options: [
{ label: 'Авторский', value: '14' },
{ label: 'Английский', value: '13' },
{ label: 'Китай', value: '12' },
{ label: 'Корея', value: '11' },
{ label: 'Фанфик', value: '15' },
{ label: 'Япония', value: '10' },
{ label: 'Корея', value: '11' },
{ label: 'Китай', value: '12' },
{ label: 'Английский', value: '13' },
{ label: 'Авторский', value: '14' },
{ label: 'Фанфик', value: '15' },
],
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: {
scanlateStatus: {
label: 'Статус перевода',
value: [],
options: [
@@ -366,6 +313,7 @@ class RLIB implements Plugin.PluginBase {
{ label: 'Ужасы', value: '67' },
{ label: 'Фантастика', value: '68' },
{ label: 'Фэнтези', value: '69' },
{ label: 'Хентай', value: '84' },
{ label: 'Школа', value: '70' },
{ label: 'Эротика', value: '71' },
{ label: 'Этти', value: '72' },
@@ -379,7 +327,7 @@ class RLIB implements Plugin.PluginBase {
value: { include: [], exclude: [] },
options: [
{ label: 'Авантюристы', value: '328' },
{ label: 'Антигерой', value: '176' },
{ label: 'Антигерой', value: '175' },
{ label: 'Бессмертные', value: '333' },
{ label: 'Боги', value: '218' },
{ label: 'Борьба за власть', value: '309' },
@@ -462,85 +410,46 @@ class RLIB implements Plugin.PluginBase {
export default new RLIB();
interface responseBook {
hasStickyPermission: boolean;
bookmark?: null;
auth: boolean;
comments_version: string;
manga: Manga;
chapters: Chapters;
user?: User;
}
interface Manga {
interface resNovels {
id: number;
name: string;
rusName?: string;
rus_name?: string;
engName?: string;
rus_name: string;
eng_name: string;
slug: string;
status: number;
chapters_count: number;
altNames?: string[] | null;
coverImage?: string;
href?: string;
slug_url: string;
cover: Cover;
ageRestriction: AgeRestrictionOrTypeOrStatus;
site: number;
type: AgeRestrictionOrTypeOrStatus;
rating: Rating;
is_licensed: boolean;
model: string;
status: AgeRestrictionOrTypeOrStatus;
releaseDateString: string;
}
interface Chapters {
list?: ListEntity[];
teams?: TeamsEntity[];
branches?: BranchesEntity[];
is_paid?: string[] | null;
interface Cover {
filename: string;
thumbnail: string;
default: string;
}
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;
interface AgeRestrictionOrTypeOrStatus {
id: number;
branch_id: number;
sale: number;
href: string;
pivot: Pivot;
label: string;
}
interface Pivot {
manga_id: number;
team_id: number;
interface Rating {
average: string;
averageFormated: string;
votes: number;
votesFormated: string;
user: number;
}
interface BranchesEntity {
interface resChapters {
id: number;
manga_id: number;
index: number;
item_number: number;
volume: string;
number: string;
number_secondary: string;
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;
}
+73 -64
View File
@@ -3,14 +3,16 @@ import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
const regex =
/<script id="__NEXT_DATA__" type="application\/json">(\{.*?\})<\/script>/;
class RNRF implements Plugin.PluginBase {
id = 'RNRF';
name = 'РанобэРФ';
site = 'https://ранобэ.рф';
version = '1.0.0';
version = '1.0.1';
icon = 'src/ru/ranoberf/icon.png';
async popularNovels(
@@ -23,97 +25,103 @@ class RNRF implements Plugin.PluginBase {
: filters?.sort?.value || 'popular';
url += '&page=' + pageNo;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
const body = await fetchApi(url).then(res => res.text());
const novels: Plugin.NovelItem[] = [];
json.props.pageProps?.totalData?.items?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: '/' + novel.slug,
}),
);
const jsonRaw = body.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
const json: response = JSON.parse(jsonRaw[1]);
json.props.pageProps?.totalData?.items?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: '/' + novel.slug,
}),
);
}
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const book = (JSON.parse(jsonRaw || '{}') as response).props.pageProps.book;
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: book?.title || '',
cover: book?.verticalImage?.url
? this.site + book.verticalImage.url
: defaultCover,
summary: book?.description,
author: book?.author || '',
genres: book?.genres.map(item => item.title).join(', '),
status: book?.additionalInfo.includes('Активен')
? NovelStatus.Ongoing
: NovelStatus.Completed,
name: '',
};
const chapters: Plugin.ChapterItem[] = [];
const jsonRaw = body.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
const book: PagePropsBook = JSON.parse(jsonRaw[1]).props.pageProps.book;
book?.chapters.forEach((chapter, chapterIndex) => {
if (!chapter.isDonate || chapter.isUserPaid) {
chapters.push({
name: chapter.title,
path: chapter.url,
releaseTime: dayjs(chapter.publishedAt).format('LLL'),
chapterNumber: book.chapters.length - chapterIndex,
novel.name = book.title;
novel.summary = book.description;
novel.cover = book.verticalImage?.url
? this.site + book.verticalImage.url
: defaultCover;
novel.status = book?.additionalInfo.includes('Активен')
? NovelStatus.Ongoing
: NovelStatus.Completed;
if (book.author) novel.author = book.author;
if (book.genres?.length)
novel.genres = book?.genres.map(item => item.title).join(',');
if (book.chapters?.length) {
const chapters: Plugin.ChapterItem[] = [];
book.chapters.forEach((chapter, chapterIndex) => {
if (!chapter.isDonate || chapter.isUserPaid) {
chapters.push({
name: chapter.title,
path: chapter.url,
releaseTime: dayjs(chapter.publishedAt).format('LLL'),
chapterNumber: book.chapters.length - chapterIndex,
});
}
});
}
});
novel.chapters = chapters.reverse();
novel.chapters = chapters.reverse();
}
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath).then(res =>
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
let loadedCheerio = parseHTML(result);
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
const jsonRaw = body.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
let chapter: string =
JSON.parse(jsonRaw[1])?.props?.pageProps?.chapter?.content?.text || '';
loadedCheerio = parseHTML(
json.props.pageProps?.chapter?.content?.text || '',
);
loadedCheerio('img').each((index, element) => {
if (!loadedCheerio(element).attr('src')?.startsWith('http')) {
const src = loadedCheerio(element).attr('src');
loadedCheerio(element).attr('src', this.site + src);
if (chapter.includes('<img')) {
return chapter.replace(/src="(.*?)"/g, (match, url) => {
if (!url.startsWith('http')) {
return `src="${this.site}${url}"`;
}
return `src="${url}"`;
});
}
});
return chapter;
}
const chapterText = loadedCheerio.html();
return chapterText;
return '';
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`;
const result = await fetchApi(url);
const body = (await result.json()) as { items: Item[] };
const { items }: { items: Item[] } = await fetchApi(url).then(res =>
res.json(),
);
const novels: Plugin.NovelItem[] = [];
body.items.forEach(novel =>
items.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
@@ -142,6 +150,7 @@ class RNRF implements Plugin.PluginBase {
},
} satisfies Filters;
}
export default new RNRF();
interface response {
+27 -30
View File
@@ -11,7 +11,7 @@ class ReN implements Plugin.PluginBase {
name = 'Renovels';
icon = 'src/ru/renovels/icon.png';
site = 'https://renovels.org';
version = '1.0.0';
version = '1.0.1';
async popularNovels(
pageNo: number,
@@ -41,10 +41,10 @@ class ReN implements Plugin.PluginBase {
url += '&page=' + pageNo;
const result = await fetchApi(url);
const body = (await result.json()) as { content: responseNovels[] };
const novels: Plugin.NovelItem[] = body.content.map(novel => ({
const { content }: { content: responseNovels[] } = await fetchApi(url).then(
res => res.json(),
);
const novels: Plugin.NovelItem[] = content.map(novel => ({
name: novel.main_name || novel.secondary_name,
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
path: novel.dir,
@@ -54,26 +54,23 @@ class ReN implements Plugin.PluginBase {
}
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 { content }: { content: responseNovel } = await fetchApi(
this.site + '/api/titles/' + novelID,
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelID,
name:
body.content.main_name ||
body.content.secondary_name ||
body.content.another_name,
summary: body.content.description,
name: content.main_name || content.secondary_name || content.another_name,
summary: content.description,
cover:
this.site +
(body.content.img.high || body.content.img.mid || body.content.img.low),
this.site + (content.img.high || content.img.mid || content.img.low),
status:
body.content.status.name === 'Продолжается'
content.status.name === 'Продолжается'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
const tags = [body.content?.genres, body.content?.categories]
const tags = [content?.genres, content?.categories]
.flat()
.map(tags => tags?.name)
.filter(tags => tags);
@@ -83,23 +80,21 @@ class ReN implements Plugin.PluginBase {
}
const totalPages =
(body.content.branches?.[0]?.count_chapters ||
body.content.count_chapters) / 100;
const branch_id = body.content.branches?.[0]?.id || body.content.id;
(content.branches?.[0]?.count_chapters || content.count_chapters) / 100;
const branch_id = content.branches?.[0]?.id || content.id;
const chapters: Plugin.ChapterItem[] = [];
for (let page = 0; page < totalPages; page++) {
await delay(1000);
const chapterResult = await fetchApi(
const volumes: {
content: responseСhapters[];
} = await fetchApi(
this.site +
'/api/titles/chapters/?branch_id=' +
branch_id +
'&count=100&page=' +
(page + 1),
);
const volumes = (await chapterResult.json()) as {
content: responseСhapters[];
};
).then(res => res.json());
volumes.content.forEach(chapter => {
if (!chapter.is_paid || chapter.is_bought) {
@@ -124,10 +119,11 @@ class ReN implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
const url = this.site + '/api/titles/chapters/' + chapterPath.split('/')[1];
const result = await fetchApi(url);
const body = (await result.json()) as { content: responseСhapter };
const result: { content: responseСhapter } = await fetchApi(url).then(res =>
res.json(),
);
return body.content.content;
return result.content.content || '';
}
async searchNovels(
@@ -135,11 +131,12 @@ class ReN implements Plugin.PluginBase {
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/api/search/?query=${searchTerm}&count=100&field=titles&page=${pageNo}`;
const result = await fetchApi(url);
const body = (await result.json()) as { content: responseNovels[] };
const { content }: { content: responseNovels[] } = await fetchApi(url).then(
res => res.json(),
);
const novels: Plugin.NovelItem[] = [];
body.content.forEach(novel =>
content.forEach(novel =>
novels.push({
name: novel.main_name || novel.secondary_name,
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),