Plugins fixed (#903)
* test * fast fix * fix * fix * Update ranobelib.ts
This commit is contained in:
@@ -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 +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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user