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) => { loadedCheerio('.col-novel-main .list-novel .row').each((i, el) => {
const novelName = loadedCheerio(el).find('h3.novel-title > a').text(); 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') .attr('data-src')
?.replace('_200_89', ''); ?.replace('_200_89', '');
const novelUrl = loadedCheerio(el) const novelUrl = loadedCheerio(el)
@@ -146,7 +147,7 @@ class NovelBin implements Plugin.PluginBase {
return fetchFile(url, { return fetchFile(url, {
headers: { headers: {
'referrer': this.site, 'referrer': this.site,
} },
}); });
} }
+1 -1
View File
@@ -261,7 +261,7 @@ class NovelUpdates implements Plugin.PluginBase {
const parts = result.url.split('/'); const parts = result.url.split('/');
const link = `${parts[0]}//api.${parts[2]}/api/chapters/?id=${parts[3]}&num=${parts[4]}`; const link = `${parts[0]}//api.${parts[2]}/api/chapters/?id=${parts[3]}&num=${parts[4]}`;
const json = await fetchApi(link).then(r => r.json()); const json = await fetchApi(link).then(r => r.json());
chapterText = chapterText =
json.currentChapter.head + json.currentChapter.head +
`<br><hr><br>` + `<br><hr><br>` +
json.currentChapter.body + json.currentChapter.body +
+2 -2
View File
@@ -50,7 +50,7 @@ class IndoWebNovel implements Plugin.PluginBase {
if (filters.genre.value.length) if (filters.genre.value.length)
link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); 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 result = await fetchApi(link);
const body = await result.text(); const body = await result.text();
@@ -123,7 +123,7 @@ class IndoWebNovel implements Plugin.PluginBase {
link += `&order=${this.filters.sort.value}`; link += `&order=${this.filters.sort.value}`;
link += this.filters.lang.value.map(i => `&country[]=${i}`).join(''); 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 result = await fetchApi(link);
const body = await result.text(); const body = await result.text();
+55 -41
View File
@@ -2,14 +2,16 @@ import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs'; import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch'; import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const regex =
/<script id="__NEXT_DATA__" type="application\/json">(\{.*?\})<\/script>/;
class Bookriver implements Plugin.PluginBase { class Bookriver implements Plugin.PluginBase {
id = 'bookriver'; id = 'bookriver';
name = 'Bookriver'; name = 'Bookriver';
site = 'https://bookriver.ru'; site = 'https://bookriver.ru';
version = '1.0.0'; version = '1.0.1';
icon = 'src/ru/bookriver/icon.png'; icon = 'src/ru/bookriver/icon.png';
async popularNovels( async popularNovels(
@@ -29,12 +31,12 @@ class Bookriver implements Plugin.PluginBase {
} }
const result = await fetchApi(url).then(res => res.text()); const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); const jsonRaw = result.match(regex);
if (jsonRaw) { if (jsonRaw instanceof Array && jsonRaw[1]) {
const json: response = JSON.parse(jsonRaw); const json: response = JSON.parse(jsonRaw[1]);
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach(novel => json.props.pageProps.state.pagesFilter?.genre?.books?.forEach(novel =>
novels.push({ novels.push({
name: novel.name, name: novel.name,
@@ -43,6 +45,7 @@ class Bookriver implements Plugin.PluginBase {
}), }),
); );
} }
return novels; return novels;
} }
@@ -50,50 +53,60 @@ class Bookriver implements Plugin.PluginBase {
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res => const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(), 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 = { const novel: Plugin.SourceNovel = {
path: novelPath, path: novelPath,
name: book?.name || '', 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,
}; };
const chapters: Plugin.ChapterItem[] = []; const jsonRaw = result.match(regex);
book?.ebook?.chapters?.forEach((chapter, chapterIndex) => { if (jsonRaw instanceof Array && jsonRaw[1]) {
if (chapter.available) { const book: BooksEntity = JSON.parse(jsonRaw[1]).props.pageProps.state
chapters.push({ .book.bookPage;
name: chapter.name,
path: book?.slug + '/' + chapter.chapterId, novel.name = book.name || '';
releaseTime: dayjs( novel.cover = book.coverImages[0].url;
chapter.firstPublishedAt || chapter.createdAt || undefined, novel.summary = book.annotation;
).format('LLL'), novel.author = book.author?.name;
chapterNumber: chapterIndex + 1,
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; return novel;
} }
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
const url = 'https://api.bookriver.ru/api/v1/books/chapter/text/'; const url = 'https://api.bookriver.ru/api/v1/books/chapter/text/';
const result = await fetchApi(url + chapterPath.split('/').pop()); const { data }: responseChapter = await fetchApi(
const json = (await result.json()) as responseChapter; url + chapterPath.split('/').pop(),
).then(res => res.json());
let chapterText = json.data.content || 'Конец произведения'; let chapterText = data.content || 'Конец произведения';
if (json.data?.audio?.available) { if (data?.audio?.available) {
chapterText += '\n' + json.data.audio.url; chapterText += '\n' + data.audio.url;
} }
return chapterText; return chapterText;
@@ -109,11 +122,12 @@ class Bookriver implements Plugin.PluginBase {
'&page=' + '&page=' +
pageNo + pageNo +
'&perPage=10'; '&perPage=10';
const result = await fetchApi(url); const { data }: responseSearch = await fetchApi(url).then(res =>
const json = (await result.json()) as responseSearch; res.json(),
);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
json.data?.books?.forEach(novel => data?.books?.forEach(novel =>
novels.push({ novels.push({
name: novel.name, name: novel.name,
cover: novel.coverImages[0].url, cover: novel.coverImages[0].url,
+10 -7
View File
@@ -10,7 +10,7 @@ class ficbook implements Plugin.PluginBase {
id = 'ficbook'; id = 'ficbook';
name = 'ficbook'; name = 'ficbook';
site = 'https://ficbook.net'; site = 'https://ficbook.net';
version = '1.0.0'; version = '1.0.1';
icon = 'src/ru/ficbook/icon.png'; icon = 'src/ru/ficbook/icon.png';
async popularNovels( async popularNovels(
@@ -149,14 +149,17 @@ class ficbook implements Plugin.PluginBase {
formData.append('term', searchTerm); formData.append('term', searchTerm);
formData.append('page', pageNo.toString()); formData.append('page', pageNo.toString());
const result = await fetchApi(this.site + '/search/fanfic', { const { data }: { data: Data } = await fetchApi(
method: 'POST', this.site + '/search/fanfic',
body: formData, {
}); method: 'POST',
const json = (await result.json()) as { data: Data }; body: formData,
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
json.data?.data?.forEach(novel => { data?.data?.forEach(novel => {
const name = novel.title.trim(); const name = novel.title.trim();
const path = '/readfic/' + novel.slug; const path = '/readfic/' + novel.slug;
const cover = novel.cover const cover = novel.cover
+14 -12
View File
@@ -16,7 +16,7 @@ class Neobook implements Plugin.PluginBase {
id = 'neobook'; id = 'neobook';
name = 'Neobook'; name = 'Neobook';
site = 'https://neobook.org'; site = 'https://neobook.org';
version = '1.0.0'; version = '1.0.1';
icon = 'src/ru/neobook/icon.png'; icon = 'src/ru/neobook/icon.png';
async fetchNovels( async fetchNovels(
@@ -46,15 +46,17 @@ class Neobook implements Plugin.PluginBase {
); );
formData.append('filter_timeread', filters?.timeread?.value || '0-999999'); 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(
method: 'post', 'https://api.neobook.org/',
body: formData, {
}); method: 'post',
const json = (await result.json()) as { bundle_books: BundleBooks }; body: formData,
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
if (json.bundle_books.feed?.length) { if (bundle_books.feed?.length) {
json.bundle_books.feed?.forEach(novel => bundle_books.feed?.forEach(novel =>
novels.push({ novels.push({
name: novel.title, name: novel.title,
cover: novel?.attachment?.image?.m || defaultCover, cover: novel?.attachment?.image?.m || defaultCover,
@@ -91,8 +93,8 @@ class Neobook implements Plugin.PluginBase {
}; };
const bookRaw = body.match(/var postData = ({.*?});/); const bookRaw = body.match(/var postData = ({.*?});/);
if (bookRaw instanceof Array && bookRaw.length >= 2) { if (bookRaw instanceof Array && bookRaw[1]) {
const book = JSON.parse(bookRaw[1]) as Novels; const book: Novels = JSON.parse(bookRaw[1]);
novel.name = book.title; novel.name = book.title;
novel.summary = book.text?.replace?.(/<br>/g, '\n') || book.text_fix; 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 = ({.*?});/); const bookRaw = body.match(/var data = ({.*?});/);
let chapterText = ''; let chapterText = '';
if (bookRaw instanceof Array && bookRaw.length >= 2) { if (bookRaw instanceof Array && bookRaw[1]) {
const book = JSON.parse(bookRaw[1]) as Novels;
const token = chapterPath.split('=')[2]; const token = chapterPath.split('=')[2];
const book: Novels = JSON.parse(bookRaw[1]);
const chapter = book.chapters?.find?.(chapter => chapter.token == token); const chapter = book.chapters?.find?.(chapter => chapter.token == token);
chapterText = (chapter?.data?.html || '').replace(/<br>/g, ''); chapterText = (chapter?.data?.html || '').replace(/<br>/g, '');
} }
+26 -28
View File
@@ -8,7 +8,7 @@ class novelOvh implements Plugin.PluginBase {
id = 'novelovh'; id = 'novelovh';
name = 'НовелОВХ'; name = 'НовелОВХ';
site = 'https://novel.ovh'; site = 'https://novel.ovh';
version = '1.0.0'; version = '1.0.1';
icon = 'src/ru/novelovh/icon.png'; icon = 'src/ru/novelovh/icon.png';
async popularNovels( async popularNovels(
@@ -23,11 +23,12 @@ class novelOvh implements Plugin.PluginBase {
: filters?.sort?.value || 'averageRating') + : filters?.sort?.value || 'averageRating') +
',desc'; ',desc';
const result = await fetchApi(url + '&_data=routes/reader/book/index'); const { books }: { books: BooksEntity[] } = await fetchApi(
const body = (await result.json()) as { books: BooksEntity[] }; url + '&_data=routes/reader/book/index',
).then(res => res.json());
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
body.books.forEach(novel => books.forEach(novel =>
novels.push({ novels.push({
name: novel.name.ru, name: novel.name.ru,
cover: novel.poster, cover: novel.poster,
@@ -39,25 +40,22 @@ class novelOvh implements Plugin.PluginBase {
} }
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi( const { book, chapters }: responseNovel = await fetchApi(
this.resolveUrl(novelPath, true) + this.resolveUrl(novelPath, true) +
'?_data=routes/reader/book/$slug/index', '?_data=routes/reader/book/$slug/index',
); ).then(res => res.json());
const json = (await result.json()) as responseNovel;
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
path: novelPath, path: novelPath,
name: json.book.name.ru, name: book.name.ru,
cover: json.book.poster, cover: book.poster,
genres: json.book.labels?.map?.(label => label.name).join(','), genres: book.labels?.map?.(label => label.name).join(','),
summary: json.book.description, summary: book.description,
status: status:
json.book.status == 'ONGOING' book.status == 'ONGOING' ? NovelStatus.Ongoing : NovelStatus.Completed,
? NovelStatus.Ongoing
: NovelStatus.Completed,
}; };
json.book.relations?.forEach(person => { book.relations?.forEach(person => {
switch (person.type) { switch (person.type) {
case 'AUTHOR': case 'AUTHOR':
novel.author = person.publisher.name; 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.forEach((chapter, chapterIndex) =>
chapters.push({ chaptersRes.push({
name: name:
chapter.title || chapter.title ||
'Том ' + 'Том ' +
(chapter.volume || 0) + (chapter.volume || 0) +
' ' + ' ' +
(chapter.name || (chapter.name ||
'Глава ' + 'Глава ' + (chapter.number || chapters.length - chapterIndex)),
(chapter.number || json.chapters.length - chapterIndex)),
path: novelPath + '/' + chapter.id + '/0', path: novelPath + '/' + chapter.id + '/0',
releaseTime: dayjs(chapter.createdAt).format('LLL'), 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; return novel;
} }
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi( const book: responseChapter = await fetchApi(
'https://api.novel.ovh/v2/chapters/' + chapterPath.split('/')[1], 'https://api.novel.ovh/v2/chapters/' + chapterPath.split('/')[1],
); ).then(res => res.json());
const book = (await result.json()) as responseChapter;
const image = Object.fromEntries( const image = Object.fromEntries(
book?.pages?.map(({ id, image }) => [id, image]) || [], book?.pages?.map(({ id, image }) => [id, image]) || [],
); );
@@ -106,11 +103,12 @@ class novelOvh implements Plugin.PluginBase {
page: number | undefined = 1, page: number | undefined = 1,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/novel?search=${searchTerm}&page=${page - 1}`; const url = `${this.site}/novel?search=${searchTerm}&page=${page - 1}`;
const result = await fetchApi(url + '&_data=routes/reader/book/index'); const { books }: { books: BooksEntity[] } = await fetchApi(
const body = (await result.json()) as { books: BooksEntity[] }; url + '&_data=routes/reader/book/index',
).then(res => res.json());
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
body.books.forEach(novel => books.forEach(novel =>
novels.push({ novels.push({
name: novel.name.ru, name: novel.name.ru,
cover: novel.poster, cover: novel.poster,
+89 -107
View File
@@ -3,7 +3,6 @@ import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover'; import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch'; import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const statusKey: { [key: string]: string } = { const statusKey: { [key: string]: string } = {
@@ -17,40 +16,47 @@ class TL implements Plugin.PluginBase {
id = 'TL'; id = 'TL';
name = 'NovelTL'; name = 'NovelTL';
site = 'https://novel.tl'; site = 'https://novel.tl';
version = '1.0.0'; version = '1.0.1';
icon = 'src/ru/noveltl/icon.png'; icon = 'src/ru/noveltl/icon.png';
async popularNovels( async fetchNovels(
page: number, page: number,
{ filters }: Plugin.PopularNovelsOptions, { filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.site + '/api/site/v2/graphql', { const { data }: response = await fetchApi(
method: 'post', this.site + '/api/site/v2/graphql',
headers: { {
Accept: 'application/json, text/plain, */*', method: 'post',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', headers: {
'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*',
}, 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
Referer: this.site, 'Content-Type': 'application/json',
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,
}, },
}), 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[] = []; const novels: Plugin.NovelItem[] = [];
data?.projects?.content?.forEach(novel =>
json.data?.projects?.content?.forEach(novel =>
novels.push({ novels.push({
name: novel.title, name: novel.title,
path: novel.fullUrl, path: novel.fullUrl,
@@ -63,37 +69,52 @@ class TL implements Plugin.PluginBase {
return novels; 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> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + '/api/site/v2/graphql', { const { data }: response = await fetchApi(
method: 'post', this.site + '/api/site/v2/graphql',
headers: { {
Accept: 'application/json, text/plain, */*', method: 'post',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', headers: {
'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*',
}, 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
Referer: this.site, 'Content-Type': 'application/json',
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,
}, },
}), Referer: this.site,
}); body: JSON.stringify({
const json = (await result.json()) as response; 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 = { const novel: Plugin.SourceNovel = {
path: novelPath, path: novelPath,
name: json.data.project?.title || '', name: data.project?.title || '',
cover: json.data.project?.covers?.[0]?.url cover: data.project?.covers?.[0]?.url
? this.site + json.data.project.covers[0].url ? this.site + data.project.covers[0].url
: defaultCover, : defaultCover,
summary: json.data.project?.annotation?.text, summary: data.project?.annotation?.text,
status: status:
statusKey[json.data.project?.translationStatus || 'unknown'] || statusKey[data.project?.translationStatus || 'unknown'] ||
NovelStatus.Unknown, NovelStatus.Unknown,
}; };
const genres = [json.data.project?.tags, json.data.project?.genres] const genres = [data.project?.tags, data.project?.genres]
.flat() .flat()
.map(item => item?.nameRu || item?.nameEng) .map(item => item?.nameRu || item?.nameEng)
.filter(item => item); .filter(item => item);
@@ -102,7 +123,7 @@ class TL implements Plugin.PluginBase {
novel.genres = genres.join(', '); novel.genres = genres.join(', ');
} }
json.data.project?.persons?.forEach(person => { data.project?.persons?.forEach(person => {
if (person.role == 'author' && person.name.firstName) { if (person.role == 'author' && person.name.firstName) {
novel.author = novel.author =
person.name.firstName + ' ' + (person.name?.lastName || ''); person.name.firstName + ' ' + (person.name?.lastName || '');
@@ -115,7 +136,7 @@ class TL implements Plugin.PluginBase {
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
json.data.project?.subprojects?.content?.forEach(work => data.project?.subprojects?.content?.forEach(work =>
work.volumes.content.forEach((volume, volumeIndex) => work.volumes.content.forEach((volume, volumeIndex) =>
volume.chapters.forEach((chapter, chapterIndex) => { volume.chapters.forEach((chapter, chapterIndex) => {
if (chapter.published) { if (chapter.published) {
@@ -139,7 +160,7 @@ class TL implements Plugin.PluginBase {
} }
async parseChapter(chapterPath: string): Promise<string> { 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', method: 'post',
headers: { headers: {
Accept: 'application/json, text/plain, */*', Accept: 'application/json, text/plain, */*',
@@ -154,63 +175,24 @@ class TL implements Plugin.PluginBase {
url: decodeURI(chapterPath), url: decodeURI(chapterPath),
}, },
}), }),
}); }).then(res => res.json());
const json = (await result.json()) as response;
const loadedCheerio = parseHTML(json.data.chapter?.text?.text || ''); const chapterText = data.chapter?.text?.text || '';
loadedCheerio('p > a[href]').each((index, element) => { if (chapterText.includes('<img')) {
let src = loadedCheerio(element).attr('href') || ''; return chapterText.replace(
if (!src.startsWith('http')) { /src="(.*?)"|href="(.*?)"/g,
src = this.site + src; (match: string, src: string, href: string) => {
} if (src && !src.startsWith('http')) {
loadedCheerio(element).find('picture').remove(); return `src="${this.site + src}"`;
loadedCheerio(element).removeAttr('href'); } else if (href && !href.startsWith('http')) {
loadedCheerio(`<img src="${src}">`).appendTo(element); return `href="${this.site + href}"`;
}); }
return match;
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 json = (await result.json()) as response;
const novels: Plugin.NovelItem[] = [];
json?.data?.projects?.content?.forEach(novel => return chapterText;
novels.push({
name: novel.title,
path: novel.fullUrl,
cover: novel?.covers?.[0]?.url
? this.site + novel.covers[0].url
: defaultCover,
}),
);
return novels;
} }
fetchImage = fetchFile; fetchImage = fetchFile;
+37 -47
View File
@@ -2,7 +2,6 @@ import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs'; import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch'; import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const statusKey: { [key: number]: string } = { const statusKey: { [key: number]: string } = {
@@ -14,7 +13,7 @@ const statusKey: { [key: number]: string } = {
class RNBH implements Plugin.PluginBase { class RNBH implements Plugin.PluginBase {
id = 'RNBH.org'; id = 'RNBH.org';
name = 'RanobeHub'; name = 'RanobeHub';
version = '1.0.0'; version = '1.0.1';
site = 'https://ranobehub.org'; site = 'https://ranobehub.org';
icon = 'src/ru/ranobehub/icon.png'; icon = 'src/ru/ranobehub/icon.png';
@@ -58,12 +57,12 @@ class RNBH implements Plugin.PluginBase {
url += '&tags:negative=' + excludeTags.join(','); url += '&tags:negative=' + excludeTags.join(',');
} }
} }
url += '&take=40'; const { resource }: { resource: responseNovels[] } = await fetchApi(
const result = await fetchApi(url); url + '&take=40',
const body = (await result.json()) as { resource: responseNovels[] }; ).then(res => res.json());
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
body.resource.forEach(novel => resource.forEach(novel =>
novels.push({ novels.push({
name: novel.names.rus || novel.names.eng || novel.names.original, name: novel.names.rus || novel.names.eng || novel.names.original,
cover: novel.poster.medium, cover: novel.poster.medium,
@@ -75,19 +74,20 @@ class RNBH implements Plugin.PluginBase {
} }
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + '/api/ranobe/' + novelPath); const { data }: { data: responseNovel } = await fetchApi(
const json = (await result.json()) as { data: responseNovel }; this.site + '/api/ranobe/' + novelPath,
).then(res => res.json());
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
path: novelPath, path: novelPath,
name: json.data.names.rus || json.data.names.eng || '', name: data.names.rus || data.names.eng || '',
cover: json.data.posters.medium, cover: data.posters.medium,
summary: json.data.description.trim(), summary: data.description.trim(),
author: json.data?.authors?.[0]?.name_eng || '', author: data?.authors?.[0]?.name_eng || '',
status: statusKey[json.data.status.id] || NovelStatus.Unknown, 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() .flat()
.map(tags => tags?.names?.rus || tags?.names?.eng || tags?.title) .map(tags => tags?.names?.rus || tags?.names?.eng || tags?.title)
.filter(tags => tags); .filter(tags => tags);
@@ -97,21 +97,15 @@ class RNBH implements Plugin.PluginBase {
} }
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
const chaptersRaw = await fetchApi( const chaptersJSON: { volumes: VolumesEntity[] } = await fetchApi(
this.site + '/api/ranobe/' + novelPath + '/contents', this.site + '/api/ranobe/' + novelPath + '/contents',
); ).then(res => res.json());
const chaptersJSON = (await chaptersRaw.json()) as {
volumes: VolumesEntity[];
};
chaptersJSON.volumes.forEach(volume => chaptersJSON.volumes.forEach(volume =>
volume.chapters?.forEach(chapter => volume.chapters?.forEach(chapter =>
chapters.push({ chapters.push({
name: chapter.name, name: chapter.name,
path: path: novelPath + '/' + volume.num + '/' + chapter.num,
volume.num && chapter.num
? novelPath + '/' + volume.num + '/' + chapter.num
: chapter.url.replace(this.site, ''),
releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format( releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format(
'LLL', 'LLL',
), ),
@@ -125,38 +119,34 @@ class RNBH implements Plugin.PluginBase {
} }
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.resolveUrl(chapterPath)); const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
const body = await result.text(); res.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 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; return chapterText;
} }
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/api/fulltext/global?query=${searchTerm}&take=10`; const url = `${this.site}/api/fulltext/global?query=${searchTerm}&take=10`;
const result = await fetchApi(url); const result: responseSearch[] = await fetchApi(url).then(res =>
const data = (await result.json()) as responseSearch[]; res.json(),
);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
data result
?.find(item => item?.meta?.key === 'ranobe') ?.find(item => item?.meta?.key === 'ranobe')
?.data?.forEach(novel => ?.data?.forEach(novel =>
novels.push({ novels.push({
+127 -218
View File
@@ -3,20 +3,12 @@ import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch'; import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio'; 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 { class RLIB implements Plugin.PluginBase {
id = 'RLIB'; id = 'RLIB';
name = 'RanobeLib'; name = 'RanobeLib (OLD)';
site = 'https://ranobelib.me'; site = 'https://old.ranobelib.me/old/';
version = '1.0.0'; version = '1.1.0';
icon = 'src/ru/ranobelib/icon.png'; icon = 'src/ru/ranobelib/icon.png';
ui: string | undefined = undefined; ui: string | undefined = undefined;
@@ -27,51 +19,71 @@ class RLIB implements Plugin.PluginBase {
filters, filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>, }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
let url = this.site + '/manga-list?sort='; let url = this.site + 'manga-list?page=' + pageNo;
url += showLatestNovels url +=
? 'last_chapter_at' '&sort_by=' +
: filters?.sort?.value || 'rate'; (showLatestNovels
url += '&dir=' + (filters?.order?.value || 'desc'); ? 'last_chapter_at'
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 (filters?.require_chapters?.value) {
if (value instanceof Array && value.length) { url += '&chapters[min]=1';
url += '&' + type + '[]=' + value.join('&' + type + '[]='); }
} if (filters?.types?.value?.length) {
if (value?.include instanceof Array && value.include.length) { url += '&types[]=' + filters.types.value.join('&types[]=');
url += }
'&' + if (filters?.scanlateStatus?.value?.length) {
type + url +=
'[include][]=' + '&scanlateStatus[]=' +
value.include.join('&' + type + '[include][]='); filters.scanlateStatus.value.join('&scanlateStatus[]=');
} }
if (value?.exclude instanceof Array && value.exclude.length) { if (filters?.manga_status?.value?.length) {
url += url +=
'&' + '&manga_status[]=' +
type + filters.manga_status.value.join('&manga_status[]=');
'[exclude][]=' + }
value.exclude.join('&' + type + '[exclude][]=');
}
});
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 result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result); const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
this.ui = loadedCheerio('a.header-right-menu__item') this.ui = loadedCheerio('a.header-right-menu__item')
.attr('href') .attr('href')
?.replace?.(/[^0-9]/g, ''); ?.replace?.(/[^0-9]/g, '');
const novels: Plugin.NovelItem[] = []; const novelsRaw = result.match(/window\.__CATALOG_ITEMS__ = (\[.*?\]);/);
loadedCheerio('.media-card-wrap').each((index, element) => { if (novelsRaw instanceof Array && novelsRaw.length >= 2) {
const name = loadedCheerio(element).find('.media-card__title').text(); const novelsJson: resNovels[] = JSON.parse(novelsRaw[1]);
const cover = loadedCheerio(element) novelsJson.forEach(novel => {
.find('a.media-card') novels.push({
.attr('data-src'); name: novel.rus_name,
const url = loadedCheerio(element).find('a.media-card').attr('href'); cover: novel.cover.default,
if (!url) return; path: novel.slug_url,
novels.push({ name, cover, path: url.replace(this.site, '') }); });
}); });
}
return novels; return novels;
} }
@@ -97,7 +109,7 @@ class RLIB implements Plugin.PluginBase {
.replace(/ /g, ''); .replace(/ /g, '');
loadedCheerio( loadedCheerio(
'div[class="media-info-list paper"] > [class="media-info-list__item"]', 'div.media-info-list > div[class="media-info-list__item"]',
).each(function () { ).each(function () {
let name = loadedCheerio(this) let name = loadedCheerio(this)
.find('div[class="media-info-list__title"]') .find('div[class="media-info-list__title"]')
@@ -120,77 +132,26 @@ class RLIB implements Plugin.PluginBase {
.attr('href') .attr('href')
?.replace?.(/[^0-9]/g, ''); ?.replace?.(/[^0-9]/g, '');
const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/); const chaptersRaw = body.match(/window\.__CHAPTERS__ = (\[.*?\]);/);
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) { if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]); const chaptersJson: resChapters[] = JSON.parse(chaptersRaw[1]);
if (!novel.name) { if (!chaptersJson?.length) return novel;
novel.name =
chaptersJson.manga.rusName ||
chaptersJson.manga.engName ||
chaptersJson.manga.name;
}
novel.status =
statusKey[chaptersJson.manga.status] || NovelStatus.Unknown;
this.ui = chaptersJson?.user?.id;
if (!chaptersJson.chapters.list?.length) return novel;
const totalChapters = chaptersJson.chapters.list.length;
const customPage: { [key: number]: string } = {};
const customOrder: { [key: number]: number } = {};
if (
chaptersJson.chapters.branches?.length &&
chaptersJson.chapters.branches.length > 1
) {
//if the novel is being translated by more than one team
chaptersJson.chapters.branches.forEach(({ teams, id }) => {
if (teams?.length) {
customPage[id || 0] =
teams.find(team => team.is_active)?.name || teams[0].name;
}
});
//fixes the chapter's position.
chaptersJson.chapters.list.forEach(chapter => {
chapter.index = customOrder[chapter.branch_id || 0] || 1;
customOrder[chapter.branch_id || 0] =
(customOrder[chapter.branch_id || 0] || 1) + 1;
});
}
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
chaptersJson.chapters.list.forEach((chapter, chapterIndex) => chaptersJson.forEach(chapter =>
chapters.push({ chapters.push({
name: name:
'Том ' + 'Том ' +
chapter.chapter_volume + chapter.volume +
' Глава ' + ' Глава ' +
chapter.chapter_number + chapter.number +
(chapter.chapter_name ? ' ' + chapter.chapter_name.trim() : ''), (chapter.name ? ' ' + chapter.name.trim() : ''),
path: path: novelPath + '/v' + chapter.volume + '/c' + chapter.number,
novelPath + chapterNumber: chapter.index + 1,
'/v' +
chapter.chapter_volume +
'/c' +
chapter.chapter_number +
'?bid=' +
(chapter.branch_id || ''),
releaseTime: dayjs(chapter.chapter_created_at).format('LLL'),
chapterNumber:
customOrder[chapter.branch_id || 0] - (chapter.index || 0) ||
totalChapters - chapterIndex,
page: customPage[chapter.branch_id || 0] || 'Основной перевод',
}), }),
); );
novel.chapters = novel.chapters = chapters;
chaptersJson.chapters.branches?.length &&
chaptersJson.chapters.branches.length > 1
? chapters.sort((a, b) => {
if ((a.page || 0) > (b.page || 0)) return 1;
if ((a.page || 0) < (b.page || 0)) return -1;
return (a.chapterNumber || 0) - (b.chapterNumber || 0);
})
: chapters.reverse();
} }
return novel; return novel;
} }
@@ -218,17 +179,15 @@ class RLIB implements Plugin.PluginBase {
} }
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const result = await fetchApi( const result = await fetchApi(this.site + 'api/manga?q=' + searchTerm);
this.site + '/search?q=' + searchTerm + '&type=manga', const { data }: { data: resNovels[] } = await result.json();
);
const body = (await result.json()) as Manga[];
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
body.forEach(novel => data.forEach(novel =>
novels.push({ novels.push({
name: novel.rus_name || novel.name, name: novel.rus_name || novel.name,
cover: novel.coverImage, cover: novel?.cover?.default || '',
path: '/' + novel.slug, path: novel.slug_url,
}), }),
); );
@@ -237,23 +196,25 @@ class RLIB implements Plugin.PluginBase {
fetchImage = fetchFile; fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => resolveUrl = (path: string, isNovel?: boolean) =>
this.site + path + (this.ui ? (isNovel ? '?' : '&') + 'ui=' + this.ui : ''); this.site + path + (this.ui ? '?ui=' + this.ui : '');
filters = { filters = {
sort: { sort_by: {
label: 'Сортировка', label: 'Сортировка',
value: 'rate', value: 'rating_score',
options: [ options: [
{ label: 'Рейтинг', value: 'rate' }, { label: 'По рейтингу', value: 'rate_avg' },
{ label: 'Имя', value: 'name' }, { label: 'По популярности', value: 'rating_score' },
{ label: 'Просмотры', value: 'views' }, { label: 'По просмотрам', value: 'views' },
{ label: 'Дате добавления', value: 'created_at' }, { label: 'Количеству глав', value: 'chap_count' },
{ label: 'Дате обновления', value: 'last_chapter_at' }, { 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, type: FilterTypes.Picker,
}, },
order: { sort_type: {
label: 'Порядок', label: 'Порядок',
value: 'desc', value: 'desc',
options: [ options: [
@@ -262,34 +223,20 @@ class RLIB implements Plugin.PluginBase {
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
type: { types: {
label: 'Тип', label: 'Тип',
value: [], value: [],
options: [ options: [
{ label: 'Авторский', value: '14' },
{ label: 'Английский', value: '13' },
{ label: 'Китай', value: '12' },
{ label: 'Корея', value: '11' },
{ label: 'Фанфик', value: '15' },
{ label: 'Япония', value: '10' }, { label: 'Япония', value: '10' },
{ label: 'Корея', value: '11' },
{ label: 'Китай', value: '12' },
{ label: 'Английский', value: '13' },
{ label: 'Авторский', value: '14' },
{ label: 'Фанфик', value: '15' },
], ],
type: FilterTypes.CheckboxGroup, type: FilterTypes.CheckboxGroup,
}, },
format: { scanlateStatus: {
label: 'Формат выпуска',
value: { include: [], exclude: [] },
options: [
{ label: '4-кома (Ёнкома)', value: '1' },
{ label: 'В цвете', value: '4' },
{ label: 'Веб', value: '6' },
{ label: 'Вебтун', value: '7' },
{ label: 'Додзинси', value: '3' },
{ label: 'Сборник', value: '2' },
{ label: 'Сингл', value: '5' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
status: {
label: 'Статус перевода', label: 'Статус перевода',
value: [], value: [],
options: [ options: [
@@ -366,6 +313,7 @@ class RLIB implements Plugin.PluginBase {
{ label: 'Ужасы', value: '67' }, { label: 'Ужасы', value: '67' },
{ label: 'Фантастика', value: '68' }, { label: 'Фантастика', value: '68' },
{ label: 'Фэнтези', value: '69' }, { label: 'Фэнтези', value: '69' },
{ label: 'Хентай', value: '84' },
{ label: 'Школа', value: '70' }, { label: 'Школа', value: '70' },
{ label: 'Эротика', value: '71' }, { label: 'Эротика', value: '71' },
{ label: 'Этти', value: '72' }, { label: 'Этти', value: '72' },
@@ -379,7 +327,7 @@ class RLIB implements Plugin.PluginBase {
value: { include: [], exclude: [] }, value: { include: [], exclude: [] },
options: [ options: [
{ label: 'Авантюристы', value: '328' }, { label: 'Авантюристы', value: '328' },
{ label: 'Антигерой', value: '176' }, { label: 'Антигерой', value: '175' },
{ label: 'Бессмертные', value: '333' }, { label: 'Бессмертные', value: '333' },
{ label: 'Боги', value: '218' }, { label: 'Боги', value: '218' },
{ label: 'Борьба за власть', value: '309' }, { label: 'Борьба за власть', value: '309' },
@@ -462,85 +410,46 @@ class RLIB implements Plugin.PluginBase {
export default new RLIB(); export default new RLIB();
interface responseBook { interface resNovels {
hasStickyPermission: boolean;
bookmark?: null;
auth: boolean;
comments_version: string;
manga: Manga;
chapters: Chapters;
user?: User;
}
interface Manga {
id: number; id: number;
name: string; name: string;
rusName?: string; rus_name: string;
rus_name?: string; eng_name: string;
engName?: string;
slug: string; slug: string;
status: number; slug_url: string;
chapters_count: number; cover: Cover;
altNames?: string[] | null; ageRestriction: AgeRestrictionOrTypeOrStatus;
coverImage?: string; site: number;
href?: string; type: AgeRestrictionOrTypeOrStatus;
rating: Rating;
is_licensed: boolean;
model: string;
status: AgeRestrictionOrTypeOrStatus;
releaseDateString: string;
} }
interface Chapters { interface Cover {
list?: ListEntity[]; filename: string;
teams?: TeamsEntity[]; thumbnail: string;
branches?: BranchesEntity[]; default: string;
is_paid?: string[] | null;
} }
interface ListEntity { interface AgeRestrictionOrTypeOrStatus {
index?: number; //crutch
chapter_id: number;
chapter_slug: string;
chapter_name: string;
chapter_number: string;
chapter_volume: number;
chapter_moderated: number;
chapter_user_id: number;
chapter_expired_at: string;
chapter_scanlator_id: number;
chapter_created_at: string;
status?: null;
price: number;
branch_id?: number;
username: string;
}
interface TeamsEntity {
name: string;
alt_name: string;
cover: string;
slug: string;
id: number; id: number;
branch_id: number; label: string;
sale: number;
href: string;
pivot: Pivot;
} }
interface Pivot { interface Rating {
manga_id: number; average: string;
team_id: number; averageFormated: string;
votes: number;
votesFormated: string;
user: number;
} }
interface BranchesEntity {
interface resChapters {
id: number; id: number;
manga_id: number; index: number;
item_number: number;
volume: string;
number: string;
number_secondary: string;
name: string; name: string;
teams?: TeamsEntity1[] | null;
is_subscribed: boolean;
}
interface TeamsEntity1 {
id: number;
name: string;
slug: string;
cover: string;
branch_id: number;
is_active: number;
}
interface User {
id: string;
avatar: string;
access: boolean;
isAdmin: boolean;
paid: boolean;
} }
+73 -64
View File
@@ -3,14 +3,16 @@ import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover'; import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch'; import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const regex =
/<script id="__NEXT_DATA__" type="application\/json">(\{.*?\})<\/script>/;
class RNRF implements Plugin.PluginBase { class RNRF implements Plugin.PluginBase {
id = 'RNRF'; id = 'RNRF';
name = 'РанобэРФ'; name = 'РанобэРФ';
site = 'https://ранобэ.рф'; site = 'https://ранобэ.рф';
version = '1.0.0'; version = '1.0.1';
icon = 'src/ru/ranoberf/icon.png'; icon = 'src/ru/ranoberf/icon.png';
async popularNovels( async popularNovels(
@@ -23,97 +25,103 @@ class RNRF implements Plugin.PluginBase {
: filters?.sort?.value || 'popular'; : filters?.sort?.value || 'popular';
url += '&page=' + pageNo; url += '&page=' + pageNo;
const result = await fetchApi(url); const body = await fetchApi(url).then(res => res.text());
const body = await result.text();
const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
json.props.pageProps?.totalData?.items?.forEach(novel =>
novels.push({ const jsonRaw = body.match(regex);
name: novel.title, if (jsonRaw instanceof Array && jsonRaw[1]) {
cover: novel?.verticalImage?.url const json: response = JSON.parse(jsonRaw[1]);
? this.site + novel.verticalImage.url
: defaultCover, json.props.pageProps?.totalData?.items?.forEach(novel =>
path: '/' + novel.slug, novels.push({
}), name: novel.title,
); cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: '/' + novel.slug,
}),
);
}
return novels; return novels;
} }
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath); const body = await fetchApi(this.site + novelPath).then(res => res.text());
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 novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
path: novelPath, path: novelPath,
name: book?.title || '', name: '',
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,
}; };
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;
if (!chapter.isDonate || chapter.isUserPaid) { novel.summary = book.description;
chapters.push({
name: chapter.title, novel.cover = book.verticalImage?.url
path: chapter.url, ? this.site + book.verticalImage.url
releaseTime: dayjs(chapter.publishedAt).format('LLL'), : defaultCover;
chapterNumber: book.chapters.length - chapterIndex, 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; return novel;
} }
async parseChapter(chapterPath: string): Promise<string> { 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(), res.text(),
); );
let loadedCheerio = parseHTML(result); const jsonRaw = body.match(regex);
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); if (jsonRaw instanceof Array && jsonRaw[1]) {
const json: response = JSON.parse(jsonRaw || '{}'); let chapter: string =
JSON.parse(jsonRaw[1])?.props?.pageProps?.chapter?.content?.text || '';
loadedCheerio = parseHTML( if (chapter.includes('<img')) {
json.props.pageProps?.chapter?.content?.text || '', return chapter.replace(/src="(.*?)"/g, (match, url) => {
); if (!url.startsWith('http')) {
return `src="${this.site}${url}"`;
loadedCheerio('img').each((index, element) => { }
if (!loadedCheerio(element).attr('src')?.startsWith('http')) { return `src="${url}"`;
const src = loadedCheerio(element).attr('src'); });
loadedCheerio(element).attr('src', this.site + src);
} }
}); return chapter;
}
const chapterText = loadedCheerio.html(); return '';
return chapterText;
} }
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { 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 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 { items }: { items: Item[] } = await fetchApi(url).then(res =>
const body = (await result.json()) as { items: Item[] }; res.json(),
);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
body.items.forEach(novel => items.forEach(novel =>
novels.push({ novels.push({
name: novel.title, name: novel.title,
cover: novel?.verticalImage?.url cover: novel?.verticalImage?.url
@@ -142,6 +150,7 @@ class RNRF implements Plugin.PluginBase {
}, },
} satisfies Filters; } satisfies Filters;
} }
export default new RNRF(); export default new RNRF();
interface response { interface response {
+27 -30
View File
@@ -11,7 +11,7 @@ class ReN implements Plugin.PluginBase {
name = 'Renovels'; name = 'Renovels';
icon = 'src/ru/renovels/icon.png'; icon = 'src/ru/renovels/icon.png';
site = 'https://renovels.org'; site = 'https://renovels.org';
version = '1.0.0'; version = '1.0.1';
async popularNovels( async popularNovels(
pageNo: number, pageNo: number,
@@ -41,10 +41,10 @@ class ReN implements Plugin.PluginBase {
url += '&page=' + pageNo; url += '&page=' + pageNo;
const result = await fetchApi(url); const { content }: { content: responseNovels[] } = await fetchApi(url).then(
const body = (await result.json()) as { content: responseNovels[] }; res => res.json(),
);
const novels: Plugin.NovelItem[] = body.content.map(novel => ({ const novels: Plugin.NovelItem[] = content.map(novel => ({
name: novel.main_name || novel.secondary_name, name: novel.main_name || novel.secondary_name,
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low), cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
path: novel.dir, path: novel.dir,
@@ -54,26 +54,23 @@ class ReN implements Plugin.PluginBase {
} }
async parseNovel(novelID: string): Promise<Plugin.SourceNovel> { async parseNovel(novelID: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + '/api/titles/' + novelID); const { content }: { content: responseNovel } = await fetchApi(
const body = (await result.json()) as { content: responseNovel }; this.site + '/api/titles/' + novelID,
).then(res => res.json());
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
path: novelID, path: novelID,
name: name: content.main_name || content.secondary_name || content.another_name,
body.content.main_name || summary: content.description,
body.content.secondary_name ||
body.content.another_name,
summary: body.content.description,
cover: cover:
this.site + this.site + (content.img.high || content.img.mid || content.img.low),
(body.content.img.high || body.content.img.mid || body.content.img.low),
status: status:
body.content.status.name === 'Продолжается' content.status.name === 'Продолжается'
? NovelStatus.Ongoing ? NovelStatus.Ongoing
: NovelStatus.Completed, : NovelStatus.Completed,
}; };
const tags = [body.content?.genres, body.content?.categories] const tags = [content?.genres, content?.categories]
.flat() .flat()
.map(tags => tags?.name) .map(tags => tags?.name)
.filter(tags => tags); .filter(tags => tags);
@@ -83,23 +80,21 @@ class ReN implements Plugin.PluginBase {
} }
const totalPages = const totalPages =
(body.content.branches?.[0]?.count_chapters || (content.branches?.[0]?.count_chapters || content.count_chapters) / 100;
body.content.count_chapters) / 100; const branch_id = content.branches?.[0]?.id || content.id;
const branch_id = body.content.branches?.[0]?.id || body.content.id;
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
for (let page = 0; page < totalPages; page++) { for (let page = 0; page < totalPages; page++) {
await delay(1000); await delay(1000);
const chapterResult = await fetchApi( const volumes: {
content: responseСhapters[];
} = await fetchApi(
this.site + this.site +
'/api/titles/chapters/?branch_id=' + '/api/titles/chapters/?branch_id=' +
branch_id + branch_id +
'&count=100&page=' + '&count=100&page=' +
(page + 1), (page + 1),
); ).then(res => res.json());
const volumes = (await chapterResult.json()) as {
content: responseСhapters[];
};
volumes.content.forEach(chapter => { volumes.content.forEach(chapter => {
if (!chapter.is_paid || chapter.is_bought) { if (!chapter.is_paid || chapter.is_bought) {
@@ -124,10 +119,11 @@ class ReN implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
const url = this.site + '/api/titles/chapters/' + chapterPath.split('/')[1]; const url = this.site + '/api/titles/chapters/' + chapterPath.split('/')[1];
const result = await fetchApi(url); const result: { content: responseСhapter } = await fetchApi(url).then(res =>
const body = (await result.json()) as { content: responseСhapter }; res.json(),
);
return body.content.content; return result.content.content || '';
} }
async searchNovels( async searchNovels(
@@ -135,11 +131,12 @@ class ReN implements Plugin.PluginBase {
pageNo: number | undefined = 1, pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/api/search/?query=${searchTerm}&count=100&field=titles&page=${pageNo}`; const url = `${this.site}/api/search/?query=${searchTerm}&count=100&field=titles&page=${pageNo}`;
const result = await fetchApi(url); const { content }: { content: responseNovels[] } = await fetchApi(url).then(
const body = (await result.json()) as { content: responseNovels[] }; res => res.json(),
);
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
body.content.forEach(novel => content.forEach(novel =>
novels.push({ novels.push({
name: novel.main_name || novel.secondary_name, name: novel.main_name || novel.secondary_name,
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low), cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
+24 -24
View File
@@ -2,7 +2,6 @@ import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs'; import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin'; import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus'; import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export interface HotNovelPubMetadata { export interface HotNovelPubMetadata {
@@ -33,7 +32,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
this.icon = `multisrc/hotnovelpub/${metadata.id.toLowerCase()}/icon.png`; this.icon = `multisrc/hotnovelpub/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.apiSite = metadata.sourceSite.replace('://', '://api.'); this.apiSite = metadata.sourceSite.replace('://', '://api.');
this.version = '1.0.0'; this.version = '1.0.1';
this.filters = metadata.filters; this.filters = metadata.filters;
this.lang = metadata.options?.lang || 'en'; this.lang = metadata.options?.lang || 'en';
} }
@@ -50,20 +49,19 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
url += '/?page=' + (pageNo - 1) + '&limit=20'; url += '/?page=' + (pageNo - 1) + '&limit=20';
const result = await fetchApi(url, { const result: responseNovels = await fetchApi(url, {
headers: { headers: {
lang: this.lang, lang: this.lang,
}, },
}); }).then(res => res.json());
const json = (await result.json()) as responseNovels;
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
if (json.status && json.data.books.data?.length) { if (result.status && result.data.books.data?.length) {
json.data.books.data.forEach(novel => result.data.books.data.forEach(novel =>
novels.push({ novels.push({
name: novel.name, name: novel.name,
cover: this.site + novel.image, 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> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.apiSite + '/book' + novelPath, { const json: responseNovel = await fetchApi(
headers: { this.apiSite + '/book/' + novelPath,
lang: this.lang, {
headers: {
lang: this.lang,
},
}, },
}); ).then(res => res.json());
const json = (await result.json()) as responseNovel;
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
name: json.data.book.name, name: json.data.book.name,
@@ -99,7 +99,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
json.data.chapters.forEach((chapter, chapterIndex) => json.data.chapters.forEach((chapter, chapterIndex) =>
chapters.push({ chapters.push({
name: chapter.title, name: chapter.title,
path: '/' + chapter.slug, path: chapter.slug,
releaseTime: undefined, releaseTime: undefined,
chapterNumber: (chapter.index || chapterIndex) + 1, chapterNumber: (chapter.index || chapterIndex) + 1,
}), }),
@@ -111,12 +111,13 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
} }
async parseChapter(chapterPath: string): Promise<string> { 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(), 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) { if (chapterText) {
const result = await fetchApi( const result = await fetchApi(
this.site + '/server/getContent?slug=' + chapterPath, this.site + '/server/getContent?slug=' + chapterPath,
@@ -138,7 +139,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
searchTerm: string, searchTerm: string,
pageNo: number, pageNo: number,
): Promise<Plugin.NovelItem[]> { ): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.apiSite + '/search', { const result: responseSearch = await fetchApi(this.apiSite + '/search', {
headers: { headers: {
'Content-Type': 'application/json;charset=utf-8', 'Content-Type': 'application/json;charset=utf-8',
Referer: this.site, Referer: this.site,
@@ -147,16 +148,14 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
}, },
method: 'POST', method: 'POST',
body: JSON.stringify({ key_search: searchTerm }), body: JSON.stringify({ key_search: searchTerm }),
}); }).then(res => res.json());
const json = (await result.json()) as responseSearch;
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
if (json.status && json.data.books?.length) { if (result.status && result.data.books?.length) {
json.data.books.forEach(novel => result.data.books.forEach(novel =>
novels.push({ novels.push({
name: novel.name, name: novel.name,
cover: undefined, path: novel.slug,
path: '/' + novel.slug,
}), }),
); );
} }
@@ -165,6 +164,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
} }
fetchImage = fetchFile; fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => this.site + '/' + path;
} }
interface responseNovels { interface responseNovels {
+18 -12
View File
@@ -25,7 +25,7 @@ class IfreedomPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName; this.name = metadata.sourceName;
this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`; this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = '1.0.0'; this.version = '1.0.1';
this.filters = metadata.filters; this.filters = metadata.filters;
} }
@@ -139,20 +139,26 @@ class IfreedomPlugin implements Plugin.PluginBase {
const body = await fetchApi(this.site + chapterPath).then(res => const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(), 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) => { if (chapterText.includes('<img')) {
const srcset = loadedCheerio(element).attr('srcset')?.split?.(' '); return chapterText.replace(/srcset="([^"]+)"/g, (match, src) => {
if (srcset?.length) { if (!src) return match;
loadedCheerio(element).removeAttr('srcset'); const bestlink = src
const bestlink: string[] = srcset.filter(url => url.startsWith('http')); .split(' ')
if (bestlink[bestlink.length - 1]) { .filter((url: string) => url.startsWith('http'))
loadedCheerio(element).attr('src', bestlink[bestlink.length - 1]); .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; 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
+13 -39
View File
@@ -1,37 +1,20 @@
import { FilterTypes, Filters } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate'; import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import defaultSettings from './settings.json';
import { ReadwnMetadata } from './template'; import { ReadwnMetadata } from './template';
import list from './sources.json';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import path from 'path'; import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) { export const generateAll: ScrpitGeneratorFunction = function (name) {
return list return list.map((metadata: ReadwnMetadata) => {
.map<ReadwnMetadata>(p => { try {
p.filters.tags.options.unshift(...defaultSettings.filters.tags.options); const { filters }: any = require(`./filters/${metadata.id}`);
p.filters.genres.options.unshift( metadata.filters = filters;
...defaultSettings.filters.genres.options, } catch (e) {}
); console.log(
p.filters = Object.assign(defaultSettings.filters, p.filters); `[${name}] Generating: ${metadata.id}${' '.repeat(20 - metadata.id.length)} ${metadata.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`,
);
const d = false; return generator(metadata);
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);
});
}; };
const generator = function generator(metadata: ReadwnMetadata) { const generator = function generator(metadata: ReadwnMetadata) {
@@ -39,18 +22,9 @@ const generator = function generator(metadata: ReadwnMetadata) {
encoding: 'utf-8', encoding: 'utf-8',
}); });
//metadata.filters = {
// sort: { ...defaultSettings.filters.sort, type: FilterTypes.Picker },
// status: { ...defaultSettings.filters.status, type: FilterTypes.Picker },
// ...metadata.filters,
//};
const pluginScript = ` const pluginScript = `
${readwnTemplate} ${readwnTemplate}
const plugin = new ReadwnPlugin(${JSON.stringify(metadata).replace( const plugin = new ReadwnPlugin(${JSON.stringify(metadata)});
/"type":"([^"]+)"/g,
'"type":FilterTypes.$1',
)});
export default plugin; export default plugin;
`.trim(); `.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; id: string;
sourceSite: string; sourceSite: string;
sourceName: string; sourceName: string;
filters?: Filters; filters?: any;
} }
class ReadwnPlugin implements Plugin.PluginBase { class ReadwnPlugin implements Plugin.PluginBase {
@@ -26,7 +26,7 @@ class ReadwnPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName; this.name = metadata.sourceName;
this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`; this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = '1.0.0'; this.version = '1.0.1';
this.filters = metadata.filters; this.filters = metadata.filters;
} }
+7 -11
View File
@@ -25,7 +25,7 @@ class RulatePlugin implements Plugin.PluginBase {
this.name = metadata.sourceName; this.name = metadata.sourceName;
this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`; this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`;
this.site = metadata.sourceSite; this.site = metadata.sourceSite;
this.version = '1.0.0'; this.version = '1.0.1';
this.filters = metadata.filters; this.filters = metadata.filters;
} }
@@ -80,12 +80,10 @@ class RulatePlugin implements Plugin.PluginBase {
formData.append('path', novelPath); formData.append('path', novelPath);
formData.append('ok', 'Да'); formData.append('ok', 'Да');
await fetchApi(result.url, { result = await fetchApi(result.url, {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
result = await fetchApi(this.site + novelPath);
} }
const body = await result.text(); const body = await result.text();
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
@@ -183,14 +181,13 @@ class RulatePlugin implements Plugin.PluginBase {
let result = await fetchApi(this.site + chapterPath); let result = await fetchApi(this.site + chapterPath);
if (result.url.includes('mature?path=')) { if (result.url.includes('mature?path=')) {
const formData = new FormData(); const formData = new FormData();
formData.append('path', chapterPath.split('/').slice(0, 3).join('/'));
formData.append('ok', 'Да'); formData.append('ok', 'Да');
await fetchApi(result.url, { result = await fetchApi(result.url, {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
result = await fetchApi(this.site + chapterPath);
} }
const body = await result.text(); const body = await result.text();
const loadedCheerio = parseHTML(body); const loadedCheerio = parseHTML(body);
@@ -208,12 +205,11 @@ class RulatePlugin implements Plugin.PluginBase {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = []; const novels: Plugin.NovelItem[] = [];
const result = await fetchApi( const result: response[] = await fetchApi(
this.site + '/search/autocomplete?query=' + searchTerm, this.site + '/search/autocomplete?query=' + searchTerm,
); ).then(res => res.json());
const json = (await result.json()) as response[];
json.forEach(novel => { result.forEach(novel => {
const name = novel.title_one + ' / ' + novel.title_two; const name = novel.title_one + ' / ' + novel.title_two;
if (!novel.url) return; if (!novel.url) return;