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
+3 -2
View File
@@ -14,7 +14,8 @@ class NovelBin implements Plugin.PluginBase {
loadedCheerio('.col-novel-main .list-novel .row').each((i, el) => {
const novelName = loadedCheerio(el).find('h3.novel-title > a').text();
const novelCover = loadedCheerio(el).find('img.cover')
const novelCover = loadedCheerio(el)
.find('img.cover')
.attr('data-src')
?.replace('_200_89', '');
const novelUrl = loadedCheerio(el)
@@ -146,7 +147,7 @@ class NovelBin implements Plugin.PluginBase {
return fetchFile(url, {
headers: {
'referrer': this.site,
}
},
});
}
+2 -2
View File
@@ -50,7 +50,7 @@ class IndoWebNovel implements Plugin.PluginBase {
if (filters.genre.value.length)
link += filters.genre.value.map(i => `&genre[]=${i}`).join('');
*/
let link = this.site + `page/${page}/?s`
let link = this.site + `page/${page}/?s`;
const result = await fetchApi(link);
const body = await result.text();
@@ -123,7 +123,7 @@ class IndoWebNovel implements Plugin.PluginBase {
link += `&order=${this.filters.sort.value}`;
link += this.filters.lang.value.map(i => `&country[]=${i}`).join('');
*/
let link = this.site + `page/${page}/?s=${searchTerm}`
let link = this.site + `page/${page}/?s=${searchTerm}`;
const result = await fetchApi(link);
const body = await result.text();
+45 -31
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,31 +53,37 @@ 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 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) => {
book.ebook.chapters.forEach((chapter, chapterIndex) => {
if (chapter.available) {
chapters.push({
name: chapter.name,
path: book?.slug + '/' + chapter.chapterId,
path: novelPath + '/' + chapter.chapterId,
releaseTime: dayjs(
chapter.firstPublishedAt || chapter.createdAt || undefined,
).format('LLL'),
@@ -82,18 +91,22 @@ class Bookriver implements Plugin.PluginBase {
});
}
});
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,
+8 -5
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', {
const { data }: { data: Data } = await fetchApi(
this.site + '/search/fanfic',
{
method: 'POST',
body: formData,
});
const json = (await result.json()) as { data: Data };
},
).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
+12 -10
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/', {
const { bundle_books }: { bundle_books: BundleBooks } = await fetchApi(
'https://api.neobook.org/',
{
method: 'post',
body: formData,
});
const json = (await result.json()) as { bundle_books: BundleBooks };
},
).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,
+57 -75
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,14 +16,17 @@ 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', {
const { data }: response = await fetchApi(
this.site + '/api/site/v2/graphql',
{
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
@@ -37,20 +39,24 @@ class TL implements Plugin.PluginBase {
'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 || [],
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,8 +69,22 @@ 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', {
const { data }: response = await fetchApi(
this.site + '/api/site/v2/graphql',
{
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
@@ -79,21 +99,22 @@ class TL implements Plugin.PluginBase {
url: novelPath,
},
}),
});
const json = (await result.json()) as response;
},
).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;
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}"`;
}
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',
return match;
},
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 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;
+35 -45
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,
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
}
});
let chapterText = loadedCheerio('div.text:nth-child(1)').html() || '';
// Remove script tags
chapterText = chapterText?.replace(
/<\s*script[^>]*>[\s\S]*?<\/script>/gim,
'',
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({
+120 -211
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
let url = this.site + 'manga-list?page=' + pageNo;
url +=
'&sort_by=' +
(showLatestNovels
? 'last_chapter_at'
: filters?.sort?.value || 'rate';
url += '&dir=' + (filters?.order?.value || 'desc');
url += '&chapters[min]=' + (filters?.require_chapters?.value ? '1' : '0');
: 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 (filters?.require_chapters?.value) {
url += '&chapters[min]=1';
}
if (value?.include instanceof Array && value.include.length) {
if (filters?.types?.value?.length) {
url += '&types[]=' + filters.types.value.join('&types[]=');
}
if (filters?.scanlateStatus?.value?.length) {
url +=
'&' +
type +
'[include][]=' +
value.include.join('&' + type + '[include][]=');
'&scanlateStatus[]=' +
filters.scanlateStatus.value.join('&scanlateStatus[]=');
}
if (value?.exclude instanceof Array && value.exclude.length) {
if (filters?.manga_status?.value?.length) {
url +=
'&' +
type +
'[exclude][]=' +
value.exclude.join('&' + type + '[exclude][]=');
'&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;
}
+54 -45
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,14 +25,13 @@ 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[] = [];
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,
@@ -40,35 +41,39 @@ class RNRF implements Plugin.PluginBase {
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) => {
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,
@@ -80,40 +85,43 @@ class RNRF implements Plugin.PluginBase {
});
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),
+22 -22
View File
@@ -2,7 +2,6 @@ import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
export interface HotNovelPubMetadata {
@@ -33,7 +32,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
this.icon = `multisrc/hotnovelpub/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite;
this.apiSite = metadata.sourceSite.replace('://', '://api.');
this.version = '1.0.0';
this.version = '1.0.1';
this.filters = metadata.filters;
this.lang = metadata.options?.lang || 'en';
}
@@ -50,20 +49,19 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
url += '/?page=' + (pageNo - 1) + '&limit=20';
const result = await fetchApi(url, {
const result: responseNovels = await fetchApi(url, {
headers: {
lang: this.lang,
},
});
const json = (await result.json()) as responseNovels;
}).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (json.status && json.data.books.data?.length) {
json.data.books.data.forEach(novel =>
if (result.status && result.data.books.data?.length) {
result.data.books.data.forEach(novel =>
novels.push({
name: novel.name,
cover: this.site + novel.image,
path: '/' + novel.slug,
path: novel.slug,
}),
);
}
@@ -71,12 +69,14 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.apiSite + '/book' + novelPath, {
const json: responseNovel = await fetchApi(
this.apiSite + '/book/' + novelPath,
{
headers: {
lang: this.lang,
},
});
const json = (await result.json()) as responseNovel;
},
).then(res => res.json());
const novel: Plugin.SourceNovel = {
name: json.data.book.name,
@@ -99,7 +99,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
json.data.chapters.forEach((chapter, chapterIndex) =>
chapters.push({
name: chapter.title,
path: '/' + chapter.slug,
path: chapter.slug,
releaseTime: undefined,
chapterNumber: (chapter.index || chapterIndex) + 1,
}),
@@ -111,12 +111,13 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(res =>
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio('#content-item').html() || '';
let chapterText =
body.match(/<div id="content-item" ([\s\S]*?)<\/div>/g)?.[0] || '';
if (chapterText) {
const result = await fetchApi(
this.site + '/server/getContent?slug=' + chapterPath,
@@ -138,7 +139,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.apiSite + '/search', {
const result: responseSearch = await fetchApi(this.apiSite + '/search', {
headers: {
'Content-Type': 'application/json;charset=utf-8',
Referer: this.site,
@@ -147,16 +148,14 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
},
method: 'POST',
body: JSON.stringify({ key_search: searchTerm }),
});
const json = (await result.json()) as responseSearch;
}).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (json.status && json.data.books?.length) {
json.data.books.forEach(novel =>
if (result.status && result.data.books?.length) {
result.data.books.forEach(novel =>
novels.push({
name: novel.name,
cover: undefined,
path: '/' + novel.slug,
path: novel.slug,
}),
);
}
@@ -165,6 +164,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
}
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => this.site + '/' + path;
}
interface responseNovels {
+17 -11
View File
@@ -25,7 +25,7 @@ class IfreedomPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite;
this.version = '1.0.0';
this.version = '1.0.1';
this.filters = metadata.filters;
}
@@ -139,20 +139,26 @@ class IfreedomPlugin implements Plugin.PluginBase {
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
let chapterText =
body.match(/<article id="([\s\S]*?)<\/article>/g)?.[0] || '';
chapterText = chapterText.replace(/<script[^>]*>[\s\S]*?<\/script>/gim, '');
loadedCheerio('.entry-content img').each((index, element) => {
const srcset = loadedCheerio(element).attr('srcset')?.split?.(' ');
if (srcset?.length) {
loadedCheerio(element).removeAttr('srcset');
const bestlink: string[] = srcset.filter(url => url.startsWith('http'));
if (bestlink[bestlink.length - 1]) {
loadedCheerio(element).attr('src', bestlink[bestlink.length - 1]);
}
if (chapterText.includes('<img')) {
return chapterText.replace(/srcset="([^"]+)"/g, (match, src) => {
if (!src) return match;
const bestlink = src
.split(' ')
.filter((url: string) => url.startsWith('http'))
.pop();
if (bestlink) {
if (bestlink.startsWith('http')) return `src="${bestlink}"`;
return `src="${this.site}${bestlink}"`;
}
return match;
});
}
const chapterText = loadedCheerio('.entry-content').html() || '';
return chapterText;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -35
View File
@@ -1,35 +1,18 @@
import { FilterTypes, Filters } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import defaultSettings from './settings.json';
import { ReadwnMetadata } from './template';
import list from './sources.json';
import { readFileSync } from 'fs';
import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list
.map<ReadwnMetadata>(p => {
p.filters.tags.options.unshift(...defaultSettings.filters.tags.options);
p.filters.genres.options.unshift(
...defaultSettings.filters.genres.options,
return list.map((metadata: ReadwnMetadata) => {
try {
const { filters }: any = require(`./filters/${metadata.id}`);
metadata.filters = filters;
} catch (e) {}
console.log(
`[${name}] Generating: ${metadata.id}${' '.repeat(20 - metadata.id.length)} ${metadata.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`,
);
p.filters = Object.assign(defaultSettings.filters, p.filters);
const d = false;
const filters: Filters = {};
for (const k in p.filters) {
const f = p.filters[k as keyof typeof p.filters];
if (f) {
filters[k] = {
...f,
type: FilterTypes.Picker,
};
}
}
return { ...p, filters: d ? undefined : filters };
})
.map((metadata: ReadwnMetadata) => {
console.log(`[${name}]: Generating`, metadata.id);
return generator(metadata);
});
};
@@ -39,18 +22,9 @@ const generator = function generator(metadata: ReadwnMetadata) {
encoding: 'utf-8',
});
//metadata.filters = {
// sort: { ...defaultSettings.filters.sort, type: FilterTypes.Picker },
// status: { ...defaultSettings.filters.status, type: FilterTypes.Picker },
// ...metadata.filters,
//};
const pluginScript = `
${readwnTemplate}
const plugin = new ReadwnPlugin(${JSON.stringify(metadata).replace(
/"type":"([^"]+)"/g,
'"type":FilterTypes.$1',
)});
const plugin = new ReadwnPlugin(${JSON.stringify(metadata)});
export default plugin;
`.trim();
+155
View File
@@ -0,0 +1,155 @@
require('module-alias/register');
import * as fs from 'fs';
import * as cheerio from 'cheerio';
import * as path from 'path';
import { FilterTypes, FilterOption } from '@libs/filterInputs';
const type: string[] = ['genres', 'status', 'sort'];
async function getFilters(name: string, url: string) {
const html = await fetch(url + '/list/all/all-newstime-0.html').then(res =>
res.text(),
);
const $: cheerio.CheerioAPI = cheerio.load(html);
const filters: any = {
'sort': {
type: FilterTypes.Picker,
label: 'Sort By',
value: 'onclick',
options: [],
},
'status': {
type: FilterTypes.Picker,
label: 'Status',
value: 'all',
options: [],
},
'genres': {
type: FilterTypes.Picker,
label: 'Genre / Category',
value: '',
options: [],
},
'tags': {
type: FilterTypes.Picker,
label: 'Tags',
value: '',
options: [{ label: 'NONE', value: '' }],
},
};
$('ul.proplist')?.each?.(function (index, ulElement) {
if (!type[index]) return;
$(ulElement)
.find('li > a')
.each((indx, liElement) =>
filters[type[index]].options.push({
label: $(liElement).text(),
value: $(liElement).attr('href'),
}),
);
filters[type[index]].options = filters[type[index]].options.map(
(item: FilterOption) => {
let res = item.value;
if (index == 0) res = item.value.split('/')[2];
if (index == 1)
res = item.value.replace(/\/list\/all\/(.*?)-.*$/, '$1');
if (index == 2) res = item.value.replace(/.*\/all-(.*?)-.*/, '$1');
return {
label: item.label,
value: res,
};
},
) as FilterOption[];
filters[type[index]].options.sort((a: FilterOption, b: FilterOption) => {
if (a.label === 'All') {
return -1;
} else if (b.label === 'All') {
return 1;
}
return a.label.localeCompare(b.label);
});
});
const response = await fetch(url + '/browsetags/').then(res => res.text());
const loadedCheerio: cheerio.CheerioAPI = cheerio.load(response);
const allPage = loadedCheerio('.tag-letters > a')
.map((index, element) => loadedCheerio(element).attr('href'))
.get();
// ===================== tags ======================
for (let i = 0; i < allPage.length; i++) {
console.log('fetch', url + allPage[i]);
const resTags = await fetch(url + allPage[i]).then(res => res.text());
const $: cheerio.CheerioAPI = cheerio.load(resTags);
$('.tag-items > li > a').each((index, element) =>
filters['tags'].options.push({
label: $(element).text()?.trim(),
value: $(element)
.attr('href')
?.split('/')
?.pop()
?.replace('-0.html', ''),
}),
);
let nextPage = $('.pagination > li:last-child > a').attr('href');
if (nextPage) {
const allpage = parseInt(nextPage.replace(/[^0-9]/g, '') || '0', 10);
for (let pageNo = 0; pageNo < allpage; pageNo++) {
await sleep(3000);
console.log(
'fetch',
url + allPage[i].replace('-0.html', `-${pageNo + 1}.html`),
);
const resTags = await fetch(
url + allPage[i].replace('-0.html', `-${pageNo + 1}.html`),
).then(res => res.text());
const $: cheerio.CheerioAPI = cheerio.load(resTags);
$('.tag-items > li > a').each((index, element) =>
filters['tags'].options.push({
label: $(element).text()?.trim(),
value: $(element).attr('href')?.split('/')?.pop()?.split('-')?.[0],
}),
);
}
}
await sleep(3000);
}
fs.writeFileSync(
path.join(__dirname, 'filters', name + '.json'),
JSON.stringify({ filters }, null, 2),
);
console.log(`✅Filters created successfully for ${name}✅`);
}
async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function askGetFilter() {
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
});
const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K';
await readline.question(
'Enter the id of the site (same one as in sources.json): ',
async (name: string) => {
await readline.question(
EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ',
async (url: string) => {
try {
await getFilters(name, url);
} catch (e: any) {
console.error('Error while getting filters from', url);
console.log(e.message || e);
}
readline.close();
},
);
},
);
}
askGetFilter();
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -10,7 +10,7 @@ export interface ReadwnMetadata {
id: string;
sourceSite: string;
sourceName: string;
filters?: Filters;
filters?: any;
}
class ReadwnPlugin implements Plugin.PluginBase {
@@ -26,7 +26,7 @@ class ReadwnPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite;
this.version = '1.0.0';
this.version = '1.0.1';
this.filters = metadata.filters;
}
+7 -11
View File
@@ -25,7 +25,7 @@ class RulatePlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite;
this.version = '1.0.0';
this.version = '1.0.1';
this.filters = metadata.filters;
}
@@ -80,12 +80,10 @@ class RulatePlugin implements Plugin.PluginBase {
formData.append('path', novelPath);
formData.append('ok', 'Да');
await fetchApi(result.url, {
result = await fetchApi(result.url, {
method: 'POST',
body: formData,
});
result = await fetchApi(this.site + novelPath);
}
const body = await result.text();
const loadedCheerio = parseHTML(body);
@@ -183,14 +181,13 @@ class RulatePlugin implements Plugin.PluginBase {
let result = await fetchApi(this.site + chapterPath);
if (result.url.includes('mature?path=')) {
const formData = new FormData();
formData.append('path', chapterPath.split('/').slice(0, 3).join('/'));
formData.append('ok', 'Да');
await fetchApi(result.url, {
result = await fetchApi(result.url, {
method: 'POST',
body: formData,
});
result = await fetchApi(this.site + chapterPath);
}
const body = await result.text();
const loadedCheerio = parseHTML(body);
@@ -208,12 +205,11 @@ class RulatePlugin implements Plugin.PluginBase {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const result = await fetchApi(
const result: response[] = await fetchApi(
this.site + '/search/autocomplete?query=' + searchTerm,
);
const json = (await result.json()) as response[];
).then(res => res.json());
json.forEach(novel => {
result.forEach(novel => {
const name = novel.title_one + ' / ' + novel.title_two;
if (!novel.url) return;