Feat: Vite + React (#991)

* remove test_web

* integrate reactjs-vite

* update lint-staged

* simple cors proxy server which handles cookies and redirections

* overrider global fetch with proxy call

* simple examples for set cookies and fetch request

* app bar

* add redux

* loadPlugins.
error: cant load urlencode module

* add ts-node for transforming

* plugin search & select

* plugin slice

* accordion components

* path item & popular novel

* optional for filters

* search novel

* parse novel

* parseChapter

* update cookies

* default filters

* fix: import path

* bundler module resolution

* new json_plugins

* fix: generate multisrc (missing filters)

* fix: host scripts

* fix: plugin code path

* remove typechecking action
This commit is contained in:
nyagami
2024-06-16 18:13:41 +07:00
committed by GitHub
parent a4cb781f32
commit d36e8ec85f
333 changed files with 5857 additions and 28579 deletions
-283
View File
@@ -1,283 +0,0 @@
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';
class freedlit implements Plugin.PluginBase {
id = 'freedlit.space';
name = 'LitSpace';
site = 'https://freedlit.space';
version = '1.0.0';
icon = 'src/ru/freedlit/icon.png';
async popularNovels(
page: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/get-books/all/list/' + page + '?sort=';
url += showLatestNovels ? 'recent' : filters?.sort?.value || 'popular';
url += '&status=' + (filters?.status?.value || 'all');
url += '&access=' + (filters?.access?.value || 'all');
url += '&adult=' + (filters?.adult?.value || 'hide');
if (filters?.genre?.value?.include?.length) {
url += filters.genre.value.include
.map(id => '&genres_included[]=' + id)
.join('');
}
if (filters?.genre?.value?.exclude?.length) {
url += filters.genre.value.exclude
.map(id => '&genres_excluded[]=' + id)
.join('');
}
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('#bookListBlock > div > div').each((index, element) => {
const name = loadedCheerio(element).find('div > h4 > a').text()?.trim();
const cover = loadedCheerio(element)
.find('div > a > img')
.attr('src')
?.trim();
const url = loadedCheerio(element)
.find('div > h4 > a')
.attr('href')
?.trim();
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, '') });
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('.book-info h4').text(),
cover: loadedCheerio('.book-cover > div > img').attr('src')?.trim(),
summary: loadedCheerio('#nav-home').text()?.trim(),
author: loadedCheerio('.book-info h5 > a').text(),
genres: loadedCheerio('.genre-list > a')
.map((index, element) => loadedCheerio(element).text())
.get()
.join(','),
};
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('a.chapter-line').each((chapterIndex, element) => {
const name = loadedCheerio(element).find('h6').text();
const url = loadedCheerio(element).attr('href');
if (!name || !url) return;
const releaseDate = loadedCheerio(element)
.find('span[class="date"]')
.text();
chapters.push({
name,
path: url.replace(this.site, ''),
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1,
});
});
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
loadedCheerio('div.mobile-block').remove();
loadedCheerio('div.standart-block').remove();
const chapterText = loadedCheerio('div[class="chapter"]').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = this.site + '/search?query=' + searchTerm + '&type=all';
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('#bookGridBlock > div').each((index, element) => {
const name = loadedCheerio(element).find('h4 > a').text()?.trim();
const cover = loadedCheerio(element).find('a > img').attr('src')?.trim();
const url = loadedCheerio(element).find('h4 > a').attr('href')?.trim();
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, '') });
});
return novels;
}
parseDate = (dateString: string | undefined = '') => {
const months: { [key: string]: number } = {
января: 1,
февраля: 2,
марта: 3,
апреля: 4,
мая: 5,
июня: 6,
июля: 7,
августа: 8,
сентября: 9,
октября: 10,
ноября: 11,
декабря: 12,
};
const [day, month, year] = dateString.split(' ');
if (day && months[month] && year) {
return dayjs(year + '-' + months[month] + '-' + day).format('LL');
}
return dateString || null;
};
filters = {
sort: {
label: 'Сортировка:',
value: 'popular',
options: [
{ label: 'По популярности', value: 'popular' },
{ label: 'По количеству комментариев', value: 'comments' },
{ label: 'По количеству лайков', value: 'likes' },
{ label: 'По новизне', value: 'recent' },
{ label: 'По просмотрам', value: 'views' },
],
type: FilterTypes.Picker,
},
genre: {
label: 'Жанры:',
value: { include: [], exclude: [] },
options: [
{ label: 'Любой жанр', value: 'all' },
{ label: 'Альтернативная история', value: 'alternative-history' },
{ label: 'Антиутопия', value: 'dystopia' },
{ label: 'Бизнес-литература', value: 'business-literature' },
{ label: 'Боевая фантастика', value: 'combat-fiction' },
{ label: 'Боевик', value: 'action' },
{ label: 'Боевое фэнтези', value: 'combat-fantasy' },
{ label: 'Бояръ-Аниме', value: 'boyar-anime' },
{ label: 'Героическая фантастика', value: 'heroic-fiction' },
{ label: 'Героическое фэнтези', value: 'heroic-fantasy' },
{ label: 'Городское фэнтези', value: 'urban-fantasy' },
{ label: 'Гримдарк', value: 'grimdark' },
{ label: 'Детектив', value: 'mystery' },
{ label: 'Детская литература', value: 'kids-literature' },
{ label: 'Документальная проза', value: 'biography' },
{ label: 'Историческая проза', value: 'historical-fiction' },
{ label: 'Исторический детектив', value: 'historical-mystery' },
{
label: 'Исторический любовный роман',
value: 'historical-romantic-novel',
},
{ label: 'Историческое фэнтези', value: 'historical-fantasy' },
{ label: 'Киберпанк', value: 'cyberpunk' },
{ label: 'Космическая фантастика', value: 'cosmic-fiction' },
{ label: 'ЛитРПГ', value: 'litrpg' },
{ label: 'Лоу / Низкое фэнтези', value: 'low-fantasy' },
{ label: 'Любовное фэнтези', value: 'romantic-fantasy' },
{ label: 'Любовный роман', value: 'romantic-novel' },
{ label: 'Мистика', value: 'mystic' },
{ label: 'Мистический детектив', value: 'mystic-detective' },
{ label: 'Научная фантастика', value: 'science-fiction' },
{ label: 'Подростковая проза', value: 'young-adult' },
{ label: 'Политический роман', value: 'political-romance' },
{ label: 'Попаданцы', value: 'accidental-travel' },
{ label: 'Попаданцы в магические миры', value: 'magic-worlds-travel' },
{ label: 'Попаданцы во времени', value: 'time-travel' },
{ label: 'Порнотика', value: 'pornotica' },
{ label: 'Постапокалипсис', value: 'post-apocalypse' },
{ label: 'Поэзия', value: 'poetry' },
{ label: 'Приключения', value: 'adventure' },
{ label: 'Публицистика', value: 'journalism' },
{ label: 'Пьеса', value: 'play' },
{ label: 'Развитие личности', value: 'how-to-book' },
{ label: 'Разное', value: 'other' },
{ label: 'Реализм', value: 'Realism' },
{ label: 'РеалРПГ', value: 'realrpg' },
{ label: 'Репликация', value: 'replication' },
{ label: 'Романтическая эротика', value: 'romantic-erotic-fiction' },
{ label: 'Сказка', value: 'fairy-tale' },
{ label: 'Слэш', value: 'slash' },
{ label: 'Современная проза', value: 'modern-prose' },
{ label: 'Современный любовный роман', value: 'modern-romantic-novel' },
{ label: 'Социальная фантастика', value: 'social-fiction' },
{ label: 'Стимпанк', value: 'steampunk' },
{ label: 'Сценарий', value: 'scenario' },
{ label: 'Сюаньхуань', value: 'xuanhuan' },
{ label: 'Сянься', value: 'xianxia' },
{ label: 'Тёмное фэнтези', value: 'dark-fantasy' },
{ label: 'Триллер', value: 'thriller' },
{ label: 'Уся', value: 'wuxia' },
{ label: 'Фантастика', value: 'fiction' },
{ label: 'Фантастический детектив', value: 'mystery-fiction' },
{ label: 'Фанфик', value: 'fan-fiction' },
{ label: 'Фемслэш', value: 'femslash' },
{ label: 'Фэнтези', value: 'fantasy' },
{ label: 'Хоррор', value: 'horror' },
{ label: 'Шпионский детектив', value: 'spy-crime' },
{ label: 'Эпическое фэнтези', value: 'epic-fantasy' },
{ label: 'Эротика', value: 'erotic-fiction' },
{ label: 'Эротическая фантастика', value: 'erotic-fiction' },
{ label: 'Эротический фанфик', value: 'erotic-fan-fiction' },
{ label: 'Эротическое фэнтези', value: 'erotic-fantasy' },
{ label: 'Этническое фэнтези', value: 'ethnic-fantasy' },
{ label: 'Юмор', value: 'humor' },
{ label: 'Юмористическая фантастика', value: 'humor-fiction' },
{ label: 'Юмористическое фэнтези', value: 'humor-fantasy' },
{ label: 'RPS', value: 'rps' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
status: {
label: 'Статус:',
value: 'all',
options: [
{ label: 'Любой статус', value: 'all' },
{ label: 'В процессе', value: 'in-process' },
{ label: 'Завершено', value: 'finished' },
],
type: FilterTypes.Picker,
},
access: {
label: 'Доступ:',
value: 'all',
options: [
{ label: 'Любой доступ', value: 'all' },
{ label: 'Бесплатные', value: 'free' },
{ label: 'Платные', value: 'paid' },
],
type: FilterTypes.Picker,
},
adult: {
label: 'Возрастные ограничения:',
value: 'hide',
options: [
{ label: 'Скрыть 18+', value: 'hide' },
{ label: 'Показать +18', value: 'show' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new freedlit();
-615
View File
@@ -1,615 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import { storage } from '@libs/storage';
import dayjs from 'dayjs';
class AuthorToday implements Plugin.PluginBase {
id = 'AT';
name = 'Автор Тудей';
icon = 'src/ru/authortoday/icon.png';
site = 'https://author.today';
apiUrl = 'https://api.author.today/v1/';
version = '1.1.0';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.apiUrl + 'catalog/search?page=' + pageNo;
if (filters?.genre?.value) {
url += '&genre=' + filters.genre.value;
}
url +=
'&sorting=' +
(showLatestNovels ? 'recent' : filters?.sort?.value || 'popular');
url += '&form=' + (filters?.form?.value || 'any');
url += '&state=' + (filters?.state?.value || 'any');
url += '&series=' + (filters?.series?.value || 'any');
url += '&access=' + (filters?.access?.value || 'any');
url += '&promo=' + (filters?.promo?.value || 'hide');
const result: response = await fetchApi(url, {
headers: {
Authorization: 'Bearer guest',
},
}).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (result.code === 'NotFound') {
return novels;
}
result?.searchResults?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel.coverUrl
? 'https://cm.author.today/content/' + novel.coverUrl
: defaultCover,
path: novel.id.toString(),
}),
);
return novels;
}
async parseNovel(workID: string): Promise<Plugin.SourceNovel> {
if (!this.user) this.user = await this.getUser();
const book: responseBook = await fetchApi(
this.apiUrl + 'work/' + workID + '/details',
{
headers: {
Authorization: 'Bearer ' + this.user?.token || 'guest',
},
},
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: workID,
name: book.title,
cover: book.coverUrl ? book.coverUrl.split('?')[0] : defaultCover,
genres: book.tags?.join(', '),
summary: '',
author:
book.originalAuthor ||
book.authorFIO ||
book.coAuthorFIO ||
book.secondCoAuthorFIO ||
book.translator ||
'',
status: book.isFinished ? NovelStatus.Completed : NovelStatus.Ongoing,
};
if (book.annotation) {
novel.summary += book.annotation + '\n';
}
if (book.authorNotes) {
novel.summary += 'Примечания автора:\n' + book.authorNotes;
}
const chaptersJSON: ChaptersEntity[] = await fetchApi(
this.apiUrl + 'work/' + workID + '/content',
{
headers: {
Authorization: 'Bearer ' + this.user?.token || 'guest',
},
},
).then(res => res.json());
const chapters: Plugin.ChapterItem[] = [];
chaptersJSON.forEach((chapter, chapterIndex) => {
if (chapter.isAvailable && !chapter.isDraft) {
chapters.push({
name: chapter.title || 'Глава ' + (chapterIndex + 1),
path: workID + '/' + chapter.id,
releaseTime: dayjs(
chapter.publishTime || chapter.lastModificationTime,
).format('LLL'),
chapterNumber: (chapter.sortOrder || chapterIndex) + 1,
});
}
});
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const [workID, chapterID] = chapterPath.split('/');
if (!this.user) this.user = await this.getUser();
const result: encryptedСhapter = await fetchApi(
this.apiUrl + `work/${workID}/chapter/${chapterID}/text`,
{
headers: {
Authorization: 'Bearer ' + this.user?.token || 'guest',
},
},
).then(res => res.json());
if (result.code) {
return result.code + '\n' + result?.message;
}
const chapterText = decrypt(result.text, result.key, this.user?.userId);
if (chapterText.includes('<img')) {
return chapterText.replace(/src="(.*?)"/g, (match, url) => {
if (!url.startsWith('http')) {
return `src="${this.site}${url}"`;
}
return `src="${url}"`;
});
}
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
this.site + '/search?category=works&q=' + searchTerm + '&page=' + pageNo;
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('a.work-row').each((index, element) => {
const name = loadedCheerio(element)
.find('h4[class="work-title"]')
.text()
.trim();
let cover = loadedCheerio(element).find('img').attr('data-src');
const path = loadedCheerio(element).attr('href');
cover = cover ? cover.split('?')[0] : defaultCover;
if (!path) return;
novels.push({ name, cover, path: path.replace('/work/', '') });
});
return novels;
}
resolveUrl = (path: string, isNovel?: boolean) =>
isNovel ? this.site + '/work/' + path : this.site + '/reader/' + path;
user: authorization | undefined;
getUser = async () => {
let user = storage.get('user') || { userId: '', token: 'guest' };
if (user && user.userId && user.token) {
const currentUser: currentUser = await fetchApi(
this.apiUrl + 'account/current-user',
{
headers: {
Authorization: 'Bearer ' + user.token || 'guest',
},
},
).then(res => res.json());
if (currentUser?.id && !currentUser.isDisabled) return user;
storage.delete('user');
user = { userId: '', token: 'guest' };
}
const result = await fetchApi(this.site + '/account/bearer-token');
if (result.url.includes('Login?ReturnUrl=')) {
return user; //It looks like the user has lost the session
}
const loginUser: authorization = await result.json();
user = { userId: loginUser.userId, token: loginUser.token };
storage.set(
'user',
user, //for some reason they're ending an hour early.
new Date(loginUser.expires).getTime() - 1 * 60 * 60 * 1000,
);
return user; //user authorized successfully
};
filters = {
sort: {
label: 'Сортировка',
value: 'popular',
options: [
{ label: 'По популярности', value: 'popular' },
{ label: 'По количеству лайков', value: 'likes' },
{ label: 'По комментариям', value: 'comments' },
{ label: 'По новизне', value: 'recent' },
{ label: 'По просмотрам', value: 'views' },
{ label: 'Набирающие популярность', value: 'trending' },
],
type: FilterTypes.Picker,
},
genre: {
label: 'Жанры',
value: '',
options: [
{ label: 'Все', value: '' },
{ label: 'Альтернативная история', value: 'sf-history' },
{ label: 'Антиутопия', value: 'dystopia' },
{ label: 'Бизнес-литература', value: 'biznes-literatura' },
{ label: 'Боевая фантастика', value: 'sf-action' },
{ label: 'Боевик', value: 'action' },
{ label: 'Боевое фэнтези', value: 'fantasy-action' },
{ label: 'Бояръ-Аниме', value: 'boyar-anime' },
{ label: 'Героическая фантастика', value: 'sf-heroic' },
{ label: 'Героическое фэнтези', value: 'heroic-fantasy' },
{ label: 'Городское фэнтези', value: 'urban-fantasy' },
{ label: 'Детектив', value: 'detective' },
{ label: 'Детская литература', value: 'detskaya-literatura' },
{ label: 'Документальная проза', value: 'non-fiction' },
{ label: 'Историческая проза', value: 'historical-fiction' },
{ label: 'Исторические приключения', value: 'historical-adventure' },
{ label: 'Исторический детектив', value: 'historical-mystery' },
{ label: 'Исторический любовный роман', value: 'historical-romance' },
{ label: 'Историческое фэнтези', value: 'historical-fantasy' },
{ label: 'Киберпанк', value: 'cyberpunk' },
{ label: 'Короткий любовный роман', value: 'short-romance' },
{ label: 'Космическая фантастика', value: 'sf-space' },
{ label: 'ЛитРПГ', value: 'litrpg' },
{ label: 'Любовное фэнтези', value: 'love-fantasy' },
{ label: 'Любовные романы', value: 'romance' },
{ label: 'Мистика', value: 'paranormal' },
{ label: 'Назад в СССР', value: 'back-to-ussr' },
{ label: 'Научная фантастика', value: 'science-fiction' },
{ label: 'Подростковая проза', value: 'teen-prose' },
{ label: 'Политический роман', value: 'political-fiction' },
{ label: 'Попаданцы', value: 'popadantsy' },
{ label: 'Попаданцы в космос', value: 'popadantsy-v-kosmos' },
{
label: 'Попаданцы в магические миры',
value: 'popadantsy-v-magicheskie-miry',
},
{ label: 'Попаданцы во времени', value: 'popadantsy-vo-vremeni' },
{ label: 'Постапокалипсис', value: 'postapocalyptic' },
{ label: 'Поэзия', value: 'poetry' },
{ label: 'Приключения', value: 'adventure' },
{ label: 'Публицистика', value: 'publicism' },
{ label: 'Развитие личности', value: 'razvitie-lichnosti' },
{ label: 'Разное', value: 'other' },
{ label: 'РеалРПГ', value: 'realrpg' },
{ label: 'Романтическая эротика', value: 'romantic-erotika' },
{ label: 'Сказка', value: 'fairy-tale' },
{ label: 'Современная проза', value: 'modern-prose' },
{ label: 'Современный любовный роман', value: 'contemporary-romance' },
{ label: 'Социальная фантастика', value: 'sf-social' },
{ label: 'Стимпанк', value: 'steampunk' },
{ label: 'Темное фэнтези', value: 'dark-fantasy' },
{ label: 'Триллер', value: 'thriller' },
{ label: 'Ужасы', value: 'horror' },
{ label: 'Фантастика', value: 'sci-fi' },
{
label: 'Фантастический детектив',
value: 'detective-science-fiction',
},
{ label: 'Фанфик', value: 'fanfiction' },
{ label: 'Фэнтези', value: 'fantasy' },
{ label: 'Шпионский детектив', value: 'spy-mystery' },
{ label: 'Эпическое фэнтези', value: 'epic-fantasy' },
{ label: 'Эротика', value: 'erotica' },
{ label: 'Эротическая фантастика', value: 'sf-erotika' },
{ label: 'Эротический фанфик', value: 'fanfiction-erotika' },
{ label: 'Эротическое фэнтези', value: 'fantasy-erotika' },
{ label: 'Юмор', value: 'humor' },
{ label: 'Юмористическая фантастика', value: 'sf-humor' },
{ label: 'Юмористическое фэнтези', value: 'ironical-fantasy' },
],
type: FilterTypes.Picker,
},
form: {
label: 'Форма произведения',
value: 'any',
options: [
{ label: 'Любой', value: 'any' },
{ label: 'Перевод', value: 'translation' },
{ label: 'Повесть', value: 'tale' },
{ label: 'Рассказ', value: 'story' },
{ label: 'Роман', value: 'novel' },
{ label: 'Сборник поэзии', value: 'poetry' },
{ label: 'Сборник рассказов', value: 'story-book' },
],
type: FilterTypes.Picker,
},
state: {
label: 'Статус произведения',
value: 'any',
options: [
{ label: 'Любой статус', value: 'any' },
{ label: 'В процессе', value: 'in-progress' },
{ label: 'Завершено', value: 'finished' },
],
type: FilterTypes.Picker,
},
series: {
label: 'Статус цикла',
value: 'any',
options: [
{ label: 'Не важно', value: 'any' },
{ label: 'Вне цикла', value: 'out' },
{ label: 'Цикл завершен', value: 'finished' },
{ label: 'Цикл не завершен', value: 'unfinished' },
],
type: FilterTypes.Picker,
},
access: {
label: 'Тип доступа',
value: 'any',
options: [
{ label: 'Любой', value: 'any' },
{ label: 'Платный', value: 'paid' },
{ label: 'Бесплатный', value: 'free' },
],
type: FilterTypes.Picker,
},
promo: {
label: 'Промо-фрагмент',
value: 'hide',
options: [
{ label: 'Скрывать', value: 'hide' },
{ label: 'Показывать', value: 'show' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new AuthorToday();
function decrypt(
encrypt: string,
encryptedKey: string,
userId: number | string = '',
) {
const key = encryptedKey.split('').reverse().join('') + '@_@' + userId;
let text = '';
for (let i = 0; i < encrypt.length; i++) {
text += String.fromCharCode(
encrypt.charCodeAt(i) ^ key.charCodeAt(i % key.length),
);
}
return text;
}
interface authorization {
userId: number;
token: string;
issued: string;
expires: string;
}
interface currentUser {
id: number;
userName: string;
fio: string;
email: string;
avatarUrl?: null;
backgroundUrl?: null;
status?: null;
hideDislike: boolean;
hideFinished: boolean;
isBanned: boolean;
banReason?: null;
banEnd?: null;
emailConfirmed: boolean;
isDeleted: boolean;
isDisabled: boolean;
}
interface response {
searchResults?: SearchResultsEntity[] | null;
realTotalCount: number;
code?: string;
errorMessage?: null;
isLastPage: boolean;
format: Format;
duration: Duration;
isWorkMarkEnabled: boolean;
}
interface SearchResultsEntity {
id: number;
title: string;
annotation: string;
authorId: number;
authorFIO: string;
authorUserName: string;
originalAuthor?: null;
coAuthorId?: number | null;
coAuthorFIO?: string | null;
coAuthorUserName?: string | null;
coAuthorConfirmed: boolean;
seriesId: number;
seriesTitle?: string | null;
publishTime: string;
lastModificationTime: string;
finishTime?: string | null;
isDraft: boolean;
formEnum: string;
genreId: number;
firstSubGenreId: number;
secondSubGenreId?: number | null;
adultOnly: boolean;
tags?: (string | null)[] | null;
coverUrl: string;
viewCount: number;
commentCount: number;
reviewCount: number;
textLength: number;
likeCount: number;
showAuthor: boolean;
price?: number | null;
discount?: null;
discountStartDate?: null;
discountEndDate?: null;
status: string;
promoFragment: boolean;
isExclusive: boolean;
format: string;
marks?: null[] | null;
isLiked: boolean;
isWorkMarkEnabled: boolean;
workInLibraryState: string;
}
interface Format {
enumValue: string;
value: string;
title: string;
mobileTitle: string;
}
interface Duration {
value: string;
title: string;
mobileTitle: string;
}
interface responseBook {
message?: string;
chapters?: ChaptersEntity[] | null;
allowDownloads: boolean;
downloadErrorCode: string;
downloadErrorMessage: string;
privacyDownloads: string;
collectionCount: number;
annotation: string;
authorNotes: string;
atRecommendation: boolean;
seriesWorkIds?: number[] | null;
seriesWorkNumber: number;
reviewCount: number;
tags?: string[] | null;
orderId?: null;
orderStatus: string;
orderStatusMessage?: null;
contests?: null[] | null;
galleryImages?: GalleryImagesEntity[] | null;
booktrailerVideoUrl?: null;
isExclusive: boolean;
freeChapterCount: number;
promoFragment: boolean;
recommendations?: RecommendationsEntity[] | null;
linkedWork?: null;
id: number;
title: string;
coverUrl: string;
lastModificationTime: string;
lastUpdateTime: string;
finishTime?: null;
isFinished: boolean;
textLength: number;
textLengthLastRead: number;
price: number;
discount?: null;
workForm: string;
status: string;
authorId: number;
authorFIO: string;
authorUserName: string;
originalAuthor?: null;
translator?: null;
reciter?: null;
coAuthorId?: null;
coAuthorFIO?: null;
coAuthorUserName?: null;
coAuthorConfirmed: boolean;
secondCoAuthorId?: null;
secondCoAuthorFIO?: null;
secondCoAuthorUserName?: null;
secondCoAuthorConfirmed: boolean;
isPurchased: boolean;
userLikeId?: null;
lastReadTime?: null;
lastChapterId?: null;
lastChapterProgress: number;
likeCount: number;
commentCount: number;
rewardCount: number;
rewardsEnabled: boolean;
inLibraryState: string;
addedToLibraryTime: string;
updateInLibraryTime: string;
privacyDisplay: string;
state: string;
isDraft: boolean;
enableRedLine: boolean;
enableTTS: boolean;
adultOnly: boolean;
seriesId: number;
seriesOrder: number;
seriesTitle: string;
afterword: string;
seriesNextWorkId?: null;
genreId: number;
firstSubGenreId: number;
secondSubGenreId: number;
format: string;
marks?: null;
purchaseTime?: null;
likeTime?: null;
markTime?: null;
}
interface ChaptersEntity {
id: number;
workId: number;
title: string;
isDraft: boolean;
sortOrder: number;
publishTime?: string;
lastModificationTime: string;
textLength: number;
isAvailable: boolean;
}
interface GalleryImagesEntity {
id: string;
caption?: null;
url: string;
height: number;
width: number;
}
interface RecommendationsEntity {
title: string;
coverUrl: string;
authorId: number;
authorFIO: string;
authorUserName: string;
originalAuthor?: null;
coAuthorId?: null;
coAuthorFIO?: null;
coAuthorUserName?: null;
coAuthorConfirmed: boolean;
secondCoAuthorId?: null;
secondCoAuthorFIO?: null;
secondCoAuthorUserName?: null;
secondCoAuthorConfirmed: boolean;
discount?: null;
price?: number | null;
lastModificationTime: string;
finishTime: string;
finished: boolean;
isExclusive: boolean;
format: string;
status: string;
state: string;
removalReason?: null;
removalReasonComment?: null;
id: number;
}
interface encryptedСhapter {
id: number;
title: string;
isDraft: boolean;
publishTime: string;
lastModificationTime: string;
text: string;
key: string;
code?: string;
message?: string;
}
-297
View File
@@ -1,297 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
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.1';
icon = 'src/ru/bookriver/icon.png';
async popularNovels(
pageNo: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/genre?page=' + pageNo + '&perPage=24&sortingType=';
url += showLatestNovels
? 'last-update'
: filters?.sort?.value || 'bestseller';
if (filters?.genres?.value?.length) {
url += '&g=' + filters.genres.value.join(',');
}
const result = await fetchApi(url).then(res => res.text());
const novels: Plugin.NovelItem[] = [];
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,
cover: novel.coverImages[0].url,
path: novel.slug,
}),
);
}
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const novel: Plugin.SourceNovel = {
path: novelPath,
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) => {
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;
}
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const url = 'https://api.bookriver.ru/api/v1/books/chapter/text/';
const { data }: responseChapter = await fetchApi(
url + chapterPath.split('/').pop(),
).then(res => res.json());
let chapterText = data.content || 'Конец произведения';
if (data?.audio?.available) {
chapterText += '\n' + data.audio.url;
}
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
'https://api.bookriver.ru/api/v1/search/autocomplete?keyword=' +
searchTerm +
'&page=' +
pageNo +
'&perPage=10';
const { data }: responseSearch = await fetchApi(url).then(res =>
res.json(),
);
const novels: Plugin.NovelItem[] = [];
data?.books?.forEach(novel =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
path: novel.slug,
}),
);
return novels;
}
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? '/book/' : '/reader/') + path;
filters = {
sort: {
label: 'Сортировка',
value: 'bestseller',
options: [
{ label: 'Бестселлеры', value: 'bestseller' },
{ label: 'Дате добавления', value: 'newest' },
{ label: 'Дате обновления', value: 'last-update' },
],
type: FilterTypes.Picker,
},
genres: {
label: 'жанры',
value: [],
options: [
{ label: 'Альтернативная история', value: 'alternativnaya-istoriya' },
{ label: 'Боевая фантастика', value: 'boevaya-fantastika' },
{ label: 'Боевое фэнтези', value: 'boevoe-fentezi' },
{ label: 'Бытовое фэнтези', value: 'bytovoe-fentezi' },
{ label: 'Героическая фантастика', value: 'geroicheskaya-fantastika' },
{ label: 'Героическое фэнтези', value: 'geroicheskoe-fentezi' },
{ label: 'Городское фэнтези', value: 'gorodskoe-fentezi' },
{ label: 'Детектив', value: 'detektiv' },
{ label: 'Детективная фантастика', value: 'detektivnaya-fantastika' },
{ label: 'Жёсткая эротика', value: 'zhyostkaya-erotika' },
{ label: 'Исторический детектив', value: 'istoricheskii-detektiv' },
{
label: 'Исторический любовный роман',
value: 'istoricheskii-lyubovnyi-roman',
},
{ label: 'Историческое фэнтези', value: 'istoricheskoe-fentezi' },
{ label: 'Киберпанк', value: 'kiberpank' },
{ label: 'Классический детектив', value: 'klassicheskii-detektiv' },
{ label: 'Короткий любовный роман', value: 'korotkii-lyubovnyi-roman' },
{ label: 'Космическая фантастика', value: 'kosmicheskaya-fantastika' },
{ label: 'Криминальный детектив', value: 'kriminalnyi-detektiv' },
{ label: 'ЛитРПГ', value: 'litrpg' },
{ label: 'Любовная фантастика', value: 'lyubovnaya-fantastika' },
{ label: 'Любовное фэнтези', value: 'lyubovnoe-fentezi' },
{ label: 'Любовный роман', value: 'lyubovnyi-roman' },
{ label: 'Мистика', value: 'mistika' },
{ label: 'Молодежная проза', value: 'molodezhnaya-proza' },
{ label: 'Научная фантастика', value: 'nauchnaya-fantastika' },
{
label: 'Остросюжетный любовный роман',
value: 'ostrosyuzhetnyi-lyubovnyi-roman',
},
{ label: 'Политический детектив', value: 'politicheskii-detektiv' },
{ label: 'Попаданцы', value: 'popadantsy' },
{ label: 'Постапокалипсис', value: 'postapokalipsis' },
{
label: 'Приключенческое фэнтези',
value: 'priklyuchencheskoe-fentezi',
},
{ label: 'Романтическая эротика', value: 'romanticheskaya-erotika' },
{ label: 'С элементами эротики', value: 's-elementami-erotiki' },
{ label: 'Славянское фэнтези', value: 'slavyanskoe-fentezi' },
{
label: 'Современный любовный роман',
value: 'sovremennyi-lyubovnyi-roman',
},
{ label: 'Социальная фантастика', value: 'sotsialnaya-fantastika' },
{ label: 'Тёмное фэнтези', value: 'temnoe-fentezi' },
{ label: 'Фантастика', value: 'fantastika' },
{ label: 'Фэнтези', value: 'fentezi' },
{ label: 'Шпионский детектив', value: 'shpionskii-detektiv' },
{ label: 'Эпическое фэнтези', value: 'epicheskoe-fentezi' },
{ label: 'Эротика', value: 'erotika' },
{ label: 'Эротическая фантастика', value: 'eroticheskaya-fantastika' },
{ label: 'Эротический фанфик', value: 'eroticheskii-fanfik' },
{ label: 'Эротическое фэнтези', value: 'eroticheskoe-fentezi' },
{
label: 'Юмористический детектив',
value: 'yumoristicheskii-detektiv',
},
{ label: 'Юмористическое фэнтези', value: 'yumoristicheskoe-fentezi' },
],
type: FilterTypes.CheckboxGroup,
},
} satisfies Filters;
}
export default new Bookriver();
interface response {
props: {
pageProps: {
state: {
book?: {
bookPage: BooksEntity;
};
pagesFilter?: {
genre: {
books: BooksEntity[];
};
};
};
};
};
}
interface BooksEntity {
name: string;
coverImages: { url: string }[];
slug: string;
annotation?: string;
author?: {
name: string;
};
tags?: { name: string }[];
ebook?: {
chapters: Chapter[];
};
statusComplete: string;
}
interface Chapter {
name: string;
available: boolean;
firstPublishedAt?: Date | null;
createdAt?: Date | null;
chapterId: number | string;
}
interface responseChapter {
data: {
id: number;
bookId: number;
name: string;
content: string;
symbols: number;
number: number;
authorPages: number;
createdAt: string;
updatedAt: string;
firstPublishedAt: string;
status: string;
protected: boolean;
free: boolean;
audio?: {
available: boolean;
url: string;
duration: number;
};
publicationScheduledFor?: null;
};
}
interface responseSearch {
data: Data;
total: number;
}
interface Data {
books?: BooksEntity[] | null;
}
File diff suppressed because it is too large Load Diff
-308
View File
@@ -1,308 +0,0 @@
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';
class Jaomix implements Plugin.PluginBase {
id = 'jaomix.ru';
name = 'Jaomix';
site = 'https://jaomix.ru';
version = '1.0.0';
icon = 'src/ru/jaomix/icon.png';
async popularNovels(
pageNo: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/?searchrn';
if (filters?.lang?.value?.length) {
url += filters.lang.value
.map((lang, idx) => `&lang[${idx}]=${lang}`)
.join('');
}
if (filters?.genre?.value?.include?.length) {
url += filters.genre.value.include
.map((genre, idx) => `&genre[${idx}]=${genre}`)
.join('');
}
if (filters?.genre?.value?.exclude?.length) {
url += filters.genre.value.exclude
.map((genre, idx) => `&delgenre[${idx}]=del ${genre}`)
.join('');
}
url += '&sortcountchapt=' + (filters?.sortcountchapt?.value || '1');
url += '&sortdaycreate=' + (filters?.sortdaycreate?.value || '1');
url +=
'&sortby=' +
(showLatestNovels ? 'upd' : filters?.sortby?.value || 'topweek');
url += '&gpage=' + pageNo;
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
(index, element) => {
const name = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr('title');
const cover = loadedCheerio(element)
.find('div[class="img-home"] > a > img')
.attr('src')
?.replace('-150x150', '');
const url = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr('href');
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, '') });
},
);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('div[class="desc-book"] > h1').text().trim(),
cover: loadedCheerio('div[class="img-book"] > img').attr('src'),
summary: loadedCheerio('div[id="desc-tab"]').text().trim(),
};
loadedCheerio('#info-book > p').each(function () {
let text = loadedCheerio(this).text().replace(/,/g, '').split(' ');
if (text[0] === 'Автор:') {
novel.author = text.splice(1).join(' ');
} else if (text[0] === 'Жанры:') {
novel.genres = text.splice(1).join(',');
} else if (text[0] === 'Статус:') {
novel.status = text.includes('продолжается')
? NovelStatus.Ongoing
: NovelStatus.Completed;
}
});
const chapters: Plugin.ChapterItem[] = [];
const totalChapters = loadedCheerio('.download-chapter div.title').length;
loadedCheerio('.download-chapter div.title').each(
(chapterIndex, element) => {
const name = loadedCheerio(element).find('a').attr('title');
const url = loadedCheerio(element).find('a').attr('href');
if (!name || !url) return;
const releaseDate = loadedCheerio(element).find('time').text();
chapters.push({
name,
path: url.replace(this.site, ''),
releaseTime: this.parseDate(releaseDate),
chapterNumber: totalChapters - chapterIndex,
});
},
);
novel.chapters = chapters.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
loadedCheerio('div[class="adblock-service"]').remove();
const chapterText = loadedCheerio('div[class="entry-content"]').html();
return chapterText || '';
}
async searchNovels(
searchTerm: string,
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
this.site +
'/?searchrn=' +
searchTerm +
'&but=Поиск по названию&sortby=upd&gpage=' +
pageNo;
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
(index, element) => {
const name = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr('title');
const cover = loadedCheerio(element)
.find('div[class="img-home"] > a > img')
.attr('src')
?.replace('-150x150', '');
const url = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr('href');
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, '') });
},
);
return novels;
}
parseDate = (dateString: string | undefined = '') => {
const months: { [key: string]: number } = {
Янв: 1,
Фев: 2,
Мар: 3,
Апр: 4,
Май: 5,
Июн: 6,
Июл: 7,
Авг: 8,
Сен: 9,
Окт: 10,
Ноя: 11,
Дек: 12,
};
const [time, day, month, year] = dateString.split(' ');
if (time && day && months[month] && year) {
return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format(
'LLL',
);
}
return dateString || null;
};
filters = {
sortby: {
label: 'Сортировка:',
value: 'topweek',
options: [
{ label: 'Топ недели', value: 'topweek' },
{ label: 'По алфавиту', value: 'alphabet' },
{ label: 'По дате обновления', value: 'upd' },
{ label: 'По дате создания', value: 'new' },
{ label: 'По просмотрам', value: 'count' },
{ label: 'Топ года', value: 'topyear' },
{ label: 'Топ дня', value: 'topday' },
{ label: 'Топ за все время', value: 'alltime' },
{ label: 'Топ месяца', value: 'topmonth' },
],
type: FilterTypes.Picker,
},
sortdaycreate: {
label: 'Дата добавления:',
value: '1',
options: [
{ label: 'Любое', value: '1' },
{ label: 'От 120 до 180 дней', value: '1218' },
{ label: 'От 180 до 365 дней', value: '1836' },
{ label: 'От 30 до 60 дней', value: '3060' },
{ label: 'От 365 дней', value: '365' },
{ label: 'От 60 до 90 дней', value: '6090' },
{ label: 'От 90 до 120 дней', value: '9012' },
{ label: 'Послед. 30 дней', value: '30' },
],
type: FilterTypes.Picker,
},
sortcountchapt: {
label: 'Количество глав:',
value: '1',
options: [
{ label: 'Любое кол-во глав', value: '1' },
{ label: 'До 500', value: '500' },
{ label: 'От 1000 до 2000', value: '1020' },
{ label: 'От 2000 до 3000', value: '2030' },
{ label: 'От 3000 до 4000', value: '3040' },
{ label: 'От 4000', value: '400' },
{ label: 'От 500 до 1000', value: '510' },
],
type: FilterTypes.Picker,
},
genre: {
label: 'Жанры:',
value: { include: [], exclude: [] },
options: [
{ label: 'Боевые Искусства', value: 'Боевые Искусства' },
{ label: 'Виртуальный Мир', value: 'Виртуальный Мир' },
{ label: 'Гарем', value: 'Гарем' },
{ label: 'Детектив', value: 'Детектив' },
{ label: 'Драма', value: 'Драма' },
{ label: 'Игра', value: 'Игра' },
{ label: 'Истории из жизни', value: 'Истории из жизни' },
{ label: 'Исторический', value: 'Исторический' },
{ label: 'История', value: 'История' },
{ label: 'Исэкай', value: 'Исэкай' },
{ label: 'Комедия', value: 'Комедия' },
{ label: 'Меха', value: 'Меха' },
{ label: 'Мистика', value: 'Мистика' },
{ label: 'Научная Фантастика', value: 'Научная Фантастика' },
{ label: 'Повседневность', value: 'Повседневность' },
{ label: 'Постапокалипсис', value: 'Постапокалипсис' },
{ label: 'Приключения', value: 'Приключения' },
{ label: 'Психология', value: 'Психология' },
{ label: 'Романтика', value: 'Романтика' },
{ label: 'Сверхъестественное', value: 'Сверхъестественное' },
{ label: 'Сёнэн', value: 'Сёнэн' },
{ label: 'Сёнэн-ай', value: 'Сёнэн-ай' },
{ label: 'Спорт', value: 'Спорт' },
{ label: 'Сэйнэн', value: 'Сэйнэн' },
{ label: 'Сюаньхуа', value: 'Сюаньхуа' },
{ label: 'Трагедия', value: 'Трагедия' },
{ label: 'Триллер', value: 'Триллер' },
{ label: 'Фантастика', value: 'Фантастика' },
{ label: 'Фэнтези', value: 'Фэнтези' },
{ label: 'Хоррор', value: 'Хоррор' },
{ label: 'Школьная жизнь', value: 'Школьная жизнь' },
{ label: 'Шоунен', value: 'Шоунен' },
{ label: 'Экшн', value: 'Экшн' },
{ label: 'Этти', value: 'Этти' },
{ label: 'Юри', value: 'Юри' },
{ label: 'Adult', value: 'Adult' },
{ label: 'Ecchi', value: 'Ecchi' },
{ label: 'Josei', value: 'Josei' },
{ label: 'Lolicon', value: 'Lolicon' },
{ label: 'Mature', value: 'Mature' },
{ label: 'Shoujo', value: 'Shoujo' },
{ label: 'Wuxia', value: 'Wuxia' },
{ label: 'Xianxia', value: 'Xianxia' },
{ label: 'Xuanhuan', value: 'Xuanhuan' },
{ label: 'Yaoi', value: 'Yaoi' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
lang: {
label: 'Выбрать языки:',
value: [],
options: [
{ label: 'Английский', value: 'Английский' },
{ label: 'Китайский', value: 'Китайский' },
{ label: 'Корейский', value: 'Корейский' },
{ label: 'Японский', value: 'Японский' },
],
type: FilterTypes.CheckboxGroup,
},
} satisfies Filters;
}
export default new Jaomix();
-454
View File
@@ -1,454 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
const statusKey: { [key: string]: string } = {
'0': NovelStatus.Unknown,
'1': NovelStatus.Ongoing,
'2': NovelStatus.Completed,
'3': NovelStatus.OnHiatus,
'4': NovelStatus.Cancelled,
};
class Neobook implements Plugin.PluginBase {
id = 'neobook';
name = 'Neobook';
site = 'https://neobook.org';
version = '1.0.1';
icon = 'src/ru/neobook/icon.png';
async fetchNovels(
page: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> {
const formData = new FormData();
formData.append('version', '4.0');
formData.append('uid', '0');
formData.append('utoken', '');
formData.append('resource', 'general');
formData.append('action', 'get_bundle');
formData.append('bundle', 'bundle_books');
formData.append('target', 'feed');
formData.append('page', page.toString());
formData.append('filter_category_id', filters?.category?.value || '0');
formData.append('filter_completed', '-1');
formData.append('filter_search', searchTerm || '');
formData.append('filter_tags', filters?.tags?.value || '');
formData.append(
'filter_sort',
showLatestNovels ? 'new' : filters?.sort?.value || 'popular',
);
formData.append('filter_timeread', filters?.timeread?.value || '0-999999');
const { bundle_books }: { bundle_books: BundleBooks } = await fetchApi(
'https://api.neobook.org/',
{
method: 'post',
body: formData,
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (bundle_books.feed?.length) {
bundle_books.feed?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.attachment?.image?.m || defaultCover,
path: novel.token + '/',
}),
);
}
return novels;
}
popularNovels = this.fetchNovels;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const defaultOptions: any = {
filters: undefined,
showLatestNovels: false,
};
return this.fetchNovels(page, defaultOptions, searchTerm);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: '',
cover: defaultCover,
};
const bookRaw = body.match(/var postData = ({.*?});/);
if (bookRaw instanceof Array && bookRaw[1]) {
const book: Novels = JSON.parse(bookRaw[1]);
novel.name = book.title;
novel.summary = book.text?.replace?.(/<br>/g, '\n') || book.text_fix;
novel.author =
book.user && book.user.firstname && book.user.lastname
? book.user.firstname + ' ' + book.user.lastname
: book.user?.initials || '';
novel.status = statusKey[book.status || '0'] || NovelStatus.Unknown;
if (book.attachment?.image?.m) novel.cover = book.attachment.image.m;
if (book.tags?.length) novel.genres = book.tags.join(',');
const chapters: Plugin.ChapterItem[] = [];
book.chapters?.forEach((chapter, chapterIndex) => {
if (chapter.access == '1' && chapter.status == '1') {
chapters.push({
name: chapter.title || 'Глава ' + (chapterIndex + 1),
path: `?book=${book.token}&chapter=${chapter.token}`,
releaseTime: null,
chapterNumber: Number(chapter.sort) || chapterIndex + 1,
});
}
});
novel.chapters = chapters;
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const bookRaw = body.match(/var data = ({.*?});/);
let chapterText = '';
if (bookRaw instanceof Array && bookRaw[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, '');
}
return chapterText;
}
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? '/book/' : '/reader/') + path;
filters = {
sort: {
label: 'Сортировка:',
value: 'popular',
options: [
{ label: 'Сначала популярные', value: 'popular' },
{ label: 'Сначала новые', value: 'new' },
{ label: 'В случайном порядке', value: 'rand' },
],
type: FilterTypes.Picker,
},
timeread: {
label: 'Время прочтения:',
value: '0-999999',
options: [
{ label: 'Все', value: '0-999999' },
{ label: 'Менее 15 минут', value: '0-900' },
{ label: '15-30 минут', value: '900-1800' },
{ label: '30-60 минут', value: '1800-3600' },
{ label: '1-2 часа', value: '3600-7200' },
{ label: 'Более 2 часов', value: '7200-999999' },
],
type: FilterTypes.Picker,
},
category: {
label: 'Жанр:',
value: '',
options: [
{ label: 'Все', value: '' },
{ label: 'Антиутопия', value: '10' },
{ label: 'Детектив', value: '13' },
{ label: 'Детские книги', value: '14' },
{ label: 'Драма', value: '15' },
{ label: 'Другое', value: '16' },
{ label: 'История', value: '18' },
{ label: 'Мелодрама', value: '21' },
{ label: 'Мистика', value: '22' },
{ label: 'Научная фантастика', value: '23' },
{ label: 'Нон-фикшн', value: '35' },
{ label: 'Подростки и молодежь', value: '26' },
{ label: 'Постапокалипсис', value: '27' },
{ label: 'Поэзия', value: '28' },
{ label: 'Приключения', value: '29' },
{ label: 'Рассказ', value: '34' },
{ label: 'Роман', value: '36' },
{ label: 'Творчество', value: '40' },
{ label: 'Триллер', value: '91' },
{ label: 'Ужасы', value: '90' },
{ label: 'Фантастика', value: '41' },
{ label: 'Фанфик', value: '42' },
{ label: 'Фэнтези', value: '44' },
{ label: 'Эротика', value: '46' },
{ label: 'Юмор', value: '47' },
],
type: FilterTypes.Picker,
},
tags: {
label: 'Тэги:',
value: '',
type: FilterTypes.TextInput,
},
} satisfies Filters;
}
export default new Neobook();
interface BundleBooks {
categories: any[];
topSection: any[];
feed: Novels[];
}
interface BottomSection {
id: string;
boost: string;
preferenceid: string;
type_id: string;
contentTypeid: string;
name: string;
title: string;
more: string;
part: string;
items: Novels[];
}
interface Novels {
status?: string;
error?: string;
login?: Login;
requestid?: string;
bundleBooks?: BundleBooks;
available?: string;
boosted?: string;
id?: string;
token: string;
type?: string;
typeid?: string;
verified?: string;
audio?: string;
labelid?: string;
labelTitle?: string;
languageid?: string;
languageTitle?: string;
categoryid?: string;
categoryTitle?: string;
price?: Price;
priceSpecial?: Price;
priceDiscount?: string;
title: string;
text: string;
text_fix: string;
copyright?: string;
align?: string;
completed?: string;
timereadS?: string;
views?: string;
likes?: string;
comments?: string;
bookmarks?: string;
isLike?: string;
isBookmark?: string;
isPurchase?: string;
isSelf?: string;
dateEdit?: string;
datePublish?: string;
link_path?: string;
user?: NovelsUser;
attachment?: Attachment;
adult?: string;
adultPlaceholder?: string;
nft?: string;
feed?: Feed;
counters?: NovelsCounters;
rating?: Rating;
readTime?: ReadTime;
readProgress?: ReadProgress;
tags?: string[];
chapters?: Chapter[];
lastComments?: any[];
bottomSection?: BottomSection;
carouselItems?: Novels[];
book?: Novels;
activeChapterIndex?: number;
}
interface Attachment {
has: string;
id: string;
typeid: string;
userid: string;
width: string;
height: string;
duration: string;
image: Image;
video: string;
origin: string;
ext: string;
}
interface Image {
s: string;
m: string;
l: string;
has?: string;
}
interface Chapter {
id: string;
token: string;
status: string;
sort: string;
free: string;
title: string;
words: string;
access: string;
loaded: string;
data?: Data;
}
interface Data {
available: string;
access: string;
id: string;
token: string;
postid: string;
free: string;
title: string;
html: string;
dateEdit: string;
datePublish: string;
attachment: Attachment;
lastComments: any[];
}
interface NovelsCounters {
impressions: Bookmarks;
views: Bookmarks;
likes: Bookmarks;
comments: Bookmarks;
bookmarks: Bookmarks;
}
interface Bookmarks {
value: string;
formatted: string;
}
interface Feed {
name: string;
canRemoved: string;
}
interface Login {
uid: string;
utoken: string;
has: string;
banned: string;
verified: string;
acc: string;
arm: string;
grm: string;
counters: LoginCounters;
user: LoginUser;
pro: any[];
earn: any[];
boost: any[];
deposit: any[];
affiliate: any[];
preferences: any[];
}
interface LoginCounters {
notifications: string;
messages: string;
}
interface LoginUser {
id: string;
username: string;
firstname: string;
lastname: string;
photo: string;
status: string;
balance: Balance;
connectedService: string;
}
interface Balance {
neo: Price;
}
interface Price {
has: string;
value: string;
currency: Currency;
formatted: string;
deadline: string;
}
type Currency = 'NEO';
interface Rating {
value: string;
count: string;
my: string;
}
interface ReadProgress {
chapter: string;
progress: string;
}
interface ReadTime {
hours: string;
minutes: string;
}
interface NovelsUser {
available: string;
boosted: string;
id: string;
status: string;
verified: string;
metrica: string;
cover: Image;
photo: Image;
username: string;
firstname: string;
lastname: string;
initials: string;
link: string;
about: string;
subscriptions: string;
subscribers: string;
publications: string;
books: string;
poems: string;
posts: string;
drafts: string;
isPro: string;
isAdult: string;
isBanned: string;
isSubscription: string;
isBlock: string;
gr: string;
}
-421
View File
@@ -1,421 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import dayjs from 'dayjs';
class novelOvh implements Plugin.PluginBase {
id = 'novelovh';
name = 'НовелОВХ';
site = 'https://novel.ovh';
version = '1.0.2';
icon = 'src/ru/novelovh/icon.png';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/content?page=' + (pageNo - 1);
url +=
'&sort=' +
(showLatestNovels
? 'updatedAt'
: filters?.sort?.value || 'averageRating') +
',desc';
const { books }: { books: BooksEntity[] } = await fetchApi(
url + '&_data=routes/reader/book/index',
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
books.forEach(novel =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
path: novel.slug,
}),
);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const { book, chapters }: responseNovel = await fetchApi(
this.site +
'/content/' +
novelPath +
'?_data=routes/reader/book/$slug/index',
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: book.name.ru,
cover: book.poster,
genres: book.labels?.map?.(label => label.name).join(','),
summary: book.description,
status:
book.status == 'ONGOING' ? NovelStatus.Ongoing : NovelStatus.Completed,
};
book.relations?.forEach(person => {
switch (person.type) {
case 'AUTHOR':
novel.author = person.publisher.name;
case 'ARTIST':
novel.artist = person.publisher.name;
}
});
const chaptersRes: Plugin.ChapterItem[] = [];
chapters.forEach((chapter, chapterIndex) =>
chaptersRes.push({
name:
chapter.title ||
'Том ' +
(chapter.volume || 0) +
' ' +
(chapter.name ||
'Глава ' + (chapter.number || chapters.length - chapterIndex)),
path: novelPath + '/' + chapter.id + '/0',
releaseTime: dayjs(chapter.createdAt).format('LLL'),
chapterNumber: chapters.length - chapterIndex,
}),
);
novel.chapters = chaptersRes.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const book: responseChapter = await fetchApi(
'https://api.novel.ovh/v2/chapters/' + chapterPath.split('/')[1],
).then(res => res.json());
const image = Object.fromEntries(
book?.pages?.map(({ id, image }) => [id, image]) || [],
);
const chapterText = this.jsonToHtml(book.content.content || [], image);
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = `https://api.novel.ovh/v2/books?type=NOVEL&search=${searchTerm}`;
const books: BooksEntity[] = await fetchApi(url).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
books.forEach(novel =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
path: novel.slug,
}),
);
return novels;
}
jsonToHtml = (
json: ContentEntity[],
image: { [key: string]: string },
html: string = '',
) => {
json.forEach(element => {
switch (element.type) {
case 'image':
if (element.attrs?.pages?.[0]) {
html += `<img src="${image[element.attrs.pages[0]]}"/>`;
}
break;
case 'hardBreak':
html += '<br>';
break;
case 'horizontalRule':
case 'delimiter':
html += '<h2 style="text-align: center">***</h2>';
break;
case 'paragraph':
html +=
'<p' +
(element?.attrs?.textAlign
? ` style="text-align: ${element.attrs.textAlign}"`
: '') +
'>' +
(element.content
? this.jsonToHtml(element.content, image)
: '<br>') +
'</p>';
break;
case 'text':
html += element.text;
break;
default:
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
break;
}
});
return html;
};
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + '/novel/' + path;
filters = {
sort: {
label: 'Сортировка',
value: 'averageRating',
options: [
{ label: 'Кол-во просмотров', value: 'viewsCount' },
{ label: 'Кол-во лайков', value: 'likesCount' },
{ label: 'Кол-во глав', value: 'chaptersCount' },
{ label: 'Кол-во закладок', value: 'bookmarksCount' },
{ label: 'Рейтингу', value: 'averageRating' },
{ label: 'Дате создания', value: 'createdAt' },
{ label: 'Дате обновления', value: 'updatedAt' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new novelOvh();
interface BooksEntity {
id: string;
slug: string;
type: string;
serviceName: string;
poster: string;
background?: string | null;
featuredCharacter?: null;
featuredCharacterBackground?: null;
featuredCharacterAnimation?: null;
featuredCharacterAnimationWithMask?: null;
featuredCharacterAnimationFirstFrame?: null;
status: string;
contentStatus: string;
name: Name;
altNames?: AltNamesEntity[] | null;
country: string;
year: number;
formats?: null[] | null;
featured: boolean;
viewsCount: number;
likesCount: number;
bookmarksCount: number;
ratingVotesCount: number;
averageRating: number;
chaptersCount: number;
createdAt: string;
updatedAt: string;
}
interface Name {
ru: string;
original: string;
en: string;
}
interface AltNamesEntity {
language: string;
name: string;
}
interface responseNovel {
book: Book;
branches: BranchesEntity[];
chapters: ChaptersEntity[];
related?: RelatedEntityOrBook[] | null;
associated?: AssociatedEntity[] | null;
chapterLikes?: null[] | null;
covers?: CoversEntity[] | null;
promotionVideos?: null[] | null;
folderItems?: null[] | null;
contentNotices?: null[] | null;
}
interface Book {
id: string;
slug: string;
type: string;
serviceName: string;
poster: string;
background?: null;
featuredCharacter?: null;
featuredCharacterBackground?: null;
featuredCharacterAnimation?: null;
featuredCharacterAnimationWithMask?: null;
featuredCharacterAnimationFirstFrame?: null;
contentBlockedCountries?: null[] | null;
status: string;
contentStatus: string;
moderationStatus: string;
name: Name;
altNames?: null[] | null;
externalLinks?: string[] | null;
description: string;
country: string;
year: number;
formats?: null[] | null;
featured: boolean;
viewsCount: number;
viewsDayCount: number;
viewsWeekCount: number;
viewsMonthCount: number;
likesCount: number;
bookmarksCount: number;
ratingVotesCount: number;
averageRating: number;
chaptersCount: number;
labels?: LabelsEntity[] | null;
relations?: RelationsEntity[] | null;
createdById: string;
createdAt: string;
updatedAt: string;
rating?: null;
}
interface LabelsEntity {
id: string;
slug: string;
name: string;
}
interface RelationsEntity {
type: string;
publisher: Publisher;
}
interface Publisher {
id: string;
kind: string;
slug: string;
name: string;
altNames?: (AltNamesEntity | null)[] | null;
poster: string;
background?: null;
trusted: boolean;
donationsEnabled: boolean;
vkGroupId?: null;
vkGroupName?: null;
tonAddress?: null;
createdAt: string;
updatedAt: string;
}
interface BranchesEntity {
id: string;
licensed: boolean;
editorsChoice: boolean;
chaptersCount: number;
createdAt: string;
updatedAt: string;
publishers?: PublishersEntity[] | null;
subscription?: null;
bookmark?: null;
}
interface PublishersEntity {
id: string;
kind: string;
slug: string;
name: string;
altNames?: AltNamesEntity[] | null;
poster: string;
background?: null;
trusted: boolean;
donationsEnabled: boolean;
vk?: null;
tonAddress?: null;
createdAt: string;
updatedAt: string;
}
interface ChaptersEntity {
id: string;
name?: string | null;
title?: string | null;
number: number;
volume: number;
likesCount: number;
commentsCount: number;
donut: boolean;
corrupted: boolean;
createdById: string;
branchId: string;
bookId: string;
createdAt: string;
expiredAt: string;
updatedAt: string;
}
interface RelatedEntityOrBook {
id: string;
type: string;
slug: string;
poster: string;
motion?: null;
status: string;
contentStatus: string;
name: Name;
country: string;
year: number;
formats?: null[] | null;
viewsCount: number;
likesCount: number;
bookmarksCount: number;
ratingVotesCount: number;
averageRating: number;
createdAt: string;
updatedAt: string;
}
interface AssociatedEntity {
id: string;
kind: string;
createdAt: string;
updatedAt: string;
book: RelatedEntityOrBook;
}
interface CoversEntity {
id: string;
volume: number;
image: string;
createdAt: string;
updatedAt: string;
}
interface responseChapter {
id: string;
name?: string;
title: string;
number: number;
volume: number;
likesCount: number;
commentsCount: number;
moderationStatus: string;
content: Content;
pages?: PagesEntity[];
donut: boolean;
corrupted: boolean;
createdById: string;
branchId: string;
bookId: string;
publishers?: PublishersEntity[] | null;
createdAt: string;
expiredAt: string;
updatedAt: string;
}
interface Content {
type: string;
content?: ContentEntity[] | null;
}
interface ContentEntity {
text?: string | null;
type: string;
attrs?: Attrs | null;
content?: Content[] | null;
}
interface Attrs {
textAlign?: string;
pages?: [string];
}
interface PagesEntity {
id: string;
image: string;
index: number;
commentsCount: number;
height: number;
width: number;
createdAt: string;
}
-950
View File
@@ -1,950 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import dayjs from 'dayjs';
const statusKey: { [key: string]: string } = {
'active': NovelStatus.Ongoing,
'completed': NovelStatus.Completed,
'freezed': NovelStatus.OnHiatus,
'unknown': NovelStatus.Unknown,
};
class TL implements Plugin.PluginBase {
id = 'TL';
name = 'NovelTL';
site = 'https://novel.tl';
version = '1.0.1';
icon = 'src/ru/noveltl/icon.png';
async fetchNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> {
const { data }: response = await fetchApi(
this.site + '/api/site/v2/graphql',
{
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
},
Referer: this.site,
body: JSON.stringify({
query:
'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}',
variables: {
filter: {
query: searchTerm || undefined,
tags: filters.tags?.value?.length
? filters.tags.value
: undefined,
genres: filters.genres?.value?.length
? filters.genres.value
: undefined,
},
hostname: 'novel.tl',
limit: 40,
page,
},
}),
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
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;
}
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 { data }: response = await fetchApi(
this.site + '/api/site/v2/graphql',
{
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
'Content-Type': 'application/json',
},
Referer: this.site,
body: JSON.stringify({
query:
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus fullUrl covers{url}persons(langs:["ru","en","*"],roles:["author","illustrator"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{shortName chapters{title publishDate fullUrl published}}}}}}}',
variables: {
url: novelPath,
},
}),
},
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: data.project?.title || '',
cover: data.project?.covers?.[0]?.url
? this.site + data.project.covers[0].url
: defaultCover,
summary: data.project?.annotation?.text,
status:
statusKey[data.project?.translationStatus || 'unknown'] ||
NovelStatus.Unknown,
};
const genres = [data.project?.tags, data.project?.genres]
.flat()
.map(item => item?.nameRu || item?.nameEng)
.filter(item => item);
if (genres?.length) {
novel.genres = genres.join(', ');
}
data.project?.persons?.forEach(person => {
if (person.role == 'author' && person.name.firstName) {
novel.author =
person.name.firstName + ' ' + (person.name?.lastName || '');
}
if (person.role == 'illustrator' && person.name.firstName) {
novel.artist =
person.name.firstName + ' ' + (person.name?.lastName || '');
}
});
const chapters: Plugin.ChapterItem[] = [];
data.project?.subprojects?.content?.forEach(work =>
work.volumes.content.forEach((volume, volumeIndex) =>
volume.chapters.forEach((chapter, chapterIndex) => {
if (chapter.published) {
chapters.push({
name:
(volume.shortName || 'Том ' + (volumeIndex + 1)) +
' ' +
(chapter.title || 'Глава ' + (chapterIndex + 1)),
path: chapter.fullUrl,
releaseTime: dayjs(chapter.publishDate).format('LLL'),
chapterNumber: chapters.length + 1,
page: work.title || 'Основная серия',
});
}
}),
),
);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const { data } = 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($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}',
variables: {
url: decodeURI(chapterPath),
},
}),
}).then(res => res.json());
const chapterText = data.chapter?.text?.text || '';
if (chapterText.includes('<img')) {
return chapterText.replace(
/src="(.*?)"|href="(.*?)"/g,
(match: string, src: string, href: string) => {
if (src && !src.startsWith('http')) {
return `src="${this.site + src}"`;
} else if (href && !href.startsWith('http')) {
return `href="${this.site + href}"`;
}
return match;
},
);
}
return chapterText;
}
resolveUrl = (path: string, isNovel?: boolean) => 'https://' + path;
filters = {
tags: {
label: 'Тэги',
value: [],
options: [
{ label: 'Автоматы', value: '64' },
{ label: 'Агрессивные персонажи', value: '26' },
{ label: 'Ад', value: '338' },
{ label: 'Адаптация манги', value: '8' },
{ label: 'Академия', value: '5' },
{ label: 'Актёрское искусство', value: '7' },
{ label: 'Актеры озвучки', value: '746' },
{ label: 'Алхимия', value: '27' },
{ label: 'Альтернативная реальность', value: '30' },
{ label: 'Амнезия', value: '31' },
{ label: 'Анабиоз', value: '170' },
{ label: 'Анал', value: '33' },
{ label: 'Ангелы', value: '38' },
{ label: 'Андроиды', value: '37' },
{ label: 'Анти-рост персонажа', value: '25' },
{ label: 'Антигерой', value: '43' },
{ label: 'Антикварный магазин', value: '44' },
{ label: 'Антимагия', value: '41' },
{ label: 'Антиутопия', value: '225' },
{ label: 'Апатичный главный герой', value: '46' },
{ label: 'Апокалипсис', value: '47' },
{ label: 'Аристократия', value: '51' },
{ label: 'Армия', value: '53' },
{ label: 'Артефакты', value: '58' },
{ label: 'Асоциальный главный герой', value: '42' },
{ label: 'Ассасины', value: '61' },
{ label: 'Атмосфера Европы', value: '245' },
{ label: 'Банды', value: '302' },
{ label: 'БДСМ', value: '77' },
{ label: 'Бедный главный герой', value: '541' },
{ label: 'Бездушный главный герой', value: '143' },
{ label: 'Беззаботный герой', value: '115' },
{ label: 'Безответная любовь', value: '213' },
{ label: 'Безответная любовь', value: '740' },
{ label: 'Безработные', value: '377' },
{ label: 'Беременность', value: '551' },
{ label: 'Бескорыстная любовь', value: '733' },
{ label: 'Бессмертные', value: '359' },
{ label: 'Бесстрашный главный герой', value: '274' },
{ label: 'Библиотека', value: '393' },
{ label: 'Биография', value: '86' },
{ label: 'Близнецы', value: '729' },
{ label: 'Богачи', value: '753' },
{ label: 'Боги', value: '318' },
{ label: 'Богини', value: '316' },
{ label: 'Богоподобный протагонист', value: '314' },
{ label: 'Боевая академия', value: '75' },
{ label: 'Боевая парочка', value: '547' },
{ label: 'Божественная защита', value: '208' },
{ label: 'Божественная сила', value: '317' },
{ label: 'Болеющие персонажи', value: '648' },
{ label: 'Борьба за власть', value: '548' },
{ label: 'Брак по расчету', value: '55' },
{ label: 'Братство', value: '106' },
{ label: 'Братья или сёстры', value: '646' },
{ label: 'Брошенные дети', value: '1' },
{ label: 'Бывший герой', value: '290' },
{ label: 'Вампиры', value: '742' },
{ label: 'Ваншот', value: '496' },
{ label: 'Вдали от родителей', value: '3' },
{ label: 'Ведьмы', value: '756' },
{ label: 'Везучий главный герой', value: '411' },
{ label: 'Верные подданные', value: '410' },
{ label: 'Вероломные герои/плетение интриг', value: '424' },
{ label: 'Взросление персонажа', value: '24' },
{ label: 'Видит то, чего остальной люд не в силах', value: '617' },
{ label: 'Виртуальная реальность', value: '744' },
{ label: 'Владелец магазина', value: '677' },
{ label: 'Владелец уникального оружия', value: '736' },
{ label: 'Властные персонажи', value: '545' },
{ label: 'Влияние прошлого', value: '517' },
{ label: 'Внезапное обогащение', value: '691' },
{ label: 'Внезапное обретение силы', value: '690' },
{ label: 'Военные хроники', value: '749' },
{ label: 'Возвращение в родной мир', value: '587' },
{ label: 'Война', value: '750' },
{ label: 'Вокалоид', value: '745' },
{ label: 'Волшебные твари', value: '413' },
{ label: 'Воскрешение', value: '586' },
{ label: 'Воспитанный главный герой', value: '537' },
{ label: 'Воспоминания прошлого', value: '283' },
{ label: 'Враги становятся союзниками', value: '238' },
{ label: 'Врата в иной мир', value: '303' },
{ label: 'Временной парадокс', value: '711' },
{ label: 'Вторжение на Землю', value: '228' },
{ label: 'Второй шанс', value: '608' },
{ label: 'Выживание', value: '695' },
{ label: 'Вынужденные условия проживания', value: '287' },
{ label: 'Высокомерный герой', value: '56' },
{ label: 'Гадание', value: '207' },
{ label: 'Галлюцинации', value: '189' },
{ label: 'Гарем рабов', value: '656' },
{ label: 'Геймеры', value: '301' },
{ label: 'Генетические модификации', value: '306' },
{ label: 'Герои', value: '341' },
{ label: 'Герои непонятного пола', value: '36' },
{ label: 'Героиня девушка-сорванец', value: '715' },
{ label: 'Героиня красавица', value: '81' },
{ label: 'Герой влюбляется первым', value: '561' },
{ label: 'Герой использует щит', value: '636' },
{ label: 'Герой красавец', value: '329' },
{ label: 'Герой со множеством тел', value: '564' },
{ label: 'Герой-бесстыдник', value: '632' },
{ label: 'Герой-извращенец', value: '521' },
{ label: 'Герой-трудяга', value: '330' },
{ label: 'Герой-яндере', value: '421' },
{ label: 'Гетерохромия', value: '342' },
{ label: 'Гильдии', value: '324' },
{ label: 'Гиперопека', value: '509' },
{ label: 'Главная героиня — женщина', value: '277' },
{ label: 'Главный герой - гений', value: '308' },
{ label: 'Главный герой - Моб', value: '446' },
{ label: 'Главный герой — знаменитость', value: '263' },
{ label: 'Главный герой — мужчина', value: '419' },
{ label: 'Главный герой в поисках гарема', value: '331' },
{ label: 'Главный герой заранее готовится', value: '557' },
{ label: 'Главный герой не человек', value: '488' },
{ label: 'Главный герой приёмный', value: '20' },
{ label: 'Главный герой сразу силён', value: '563' },
{ label: 'Гладиаторы', value: '310' },
{ label: 'Глухой к любви герой', value: '194' },
{ label: 'Гоблины', value: '313' },
{ label: 'Големы', value: '319' },
{ label: 'Гомункул', value: '347' },
{ label: 'Горничные', value: '418' },
{ label: 'Государственные интриги', value: '212' },
{ label: 'Гринд', value: '322' },
{ label: 'Давняя травма', value: '518' },
{ label: 'Двойник', value: '97' },
{ label: 'Дворецкие', value: '111' },
{ label: 'Дворфы', value: '224' },
{ label: 'Дворяне', value: '487' },
{ label: 'Двоюродные братья или сёстры', value: '162' },
{ label: 'Девочки-волшебницы', value: '415' },
{ label: 'Демоны', value: '193' },
{ label: 'Депрессия', value: '196' },
{ label: 'Детектив', value: '198' },
{ label: 'Детективное расследование', value: '473' },
{ label: 'Детское насилие', value: '125' },
{ label: 'Детское обещание', value: '130' },
{ label: 'Дискриминация', value: '203' },
{ label: 'Длительный разрыв отношений', value: '400' },
{ label: 'Домашняя жизнь', value: '45' },
{ label: 'Драконы', value: '218' },
{ label: 'Древний Китай', value: '34' },
{ label: 'Дружба', value: '293' },
{ label: 'Друзья детства', value: '128' },
{ label: 'Друзья становятся врагами', value: '292' },
{ label: 'Духи', value: '673' },
{ label: 'Духовная сила', value: '665' },
{ label: 'Духовный наставник', value: '671' },
{ label: 'Душещипательная история', value: '335' },
{ label: 'Души', value: '666' },
{ label: 'Дьявольский путь культивации', value: '192' },
{ label: 'Ёкай', value: '763' },
{ label: 'Есть аниме-адаптация', value: '10' },
{ label: 'Есть видеоигра', value: '13' },
{ label: 'Есть дорама-адаптация', value: '11' },
{ label: 'Есть манга-адаптация', value: '14' },
{ label: 'Есть манхва-адаптация', value: '16' },
{ label: 'Есть маньхуа-адаптация', value: '15' },
{ label: 'Есть фильм', value: '17' },
{ label: 'Есть CD дорама-адаптация', value: '12' },
{ label: 'Жёсткий главный герой', value: '597' },
{ label: 'Жестокие персонажи', value: '4' },
{ label: 'Жёстокие персонажи', value: '169' },
{ label: 'Животное-спутник', value: '78' },
{ label: 'Жизнь в одиночку', value: '396' },
{ label: 'Жрецы', value: '555' },
{ label: 'Жрицы', value: '554' },
{ label: 'Забота о детях', value: '127' },
{ label: 'Заботливый главный герой', value: '116' },
{ label: 'Загадочное заболевание', value: '471' },
{ label: 'Загадочное прошлое родителей', value: '470' },
{ label: 'Заговоры', value: '154' },
{ label: 'Закалка тела', value: '96' },
{ label: 'Закон джунглей', value: '682' },
{ label: 'Закулисная борба', value: '603' },
{ label: 'Замкнутый главный герой', value: '371' },
{ label: 'Запечатанная сила', value: '607' },
{ label: 'Затворник', value: '346' },
{ label: 'Звери', value: '80' },
{ label: 'Зверолюди', value: '39' },
{ label: 'Звероподобные', value: '79' },
{ label: 'Земледелие', value: '268' },
{ label: 'Злобные благородные девы', value: '743' },
{ label: 'Зловещие организации', value: '247' },
{ label: 'Злой главный герой', value: '248' },
{ label: 'Злые боги', value: '246' },
{ label: 'Злые религии', value: '249' },
{ label: 'Знаменитости', value: '118' },
{ label: 'Знания из прошлой жизни', value: '553' },
{ label: 'Зомби', value: '767' },
{ label: 'Игра на выживание', value: '696' },
{ label: 'Игровая рейтинговая система', value: '300' },
{ label: 'Из грязи в князи', value: '542' },
{ label: 'Известные родители', value: '262' },
{ label: 'Изменение внешнего вида', value: '48' },
{ label: 'Изнасилование', value: '574' },
{ label: 'Инвалидность', value: '202' },
{ label: 'Индустриализация', value: '364' },
{ label: 'Инженер', value: '241' },
{ label: 'Инцест', value: '361' },
{ label: 'Искажённая личность', value: '730' },
{ label: 'Искусственный интеллект', value: '59' },
{ label: 'Исполнение желаний', value: '755' },
{ label: 'Каннибализм', value: '113' },
{ label: 'Карточные игры', value: '114' },
{ label: 'Кладбищенский смотритель', value: '321' },
{ label: 'Классика', value: '134' },
{ label: 'Книги', value: '99' },
{ label: 'Книги навыков', value: '654' },
{ label: 'Книжный червь', value: '100' },
{ label: 'Коллеги', value: '140' },
{ label: 'Колледж/Университет', value: '145' },
{ label: 'Командная работа', value: '701' },
{ label: 'Комедийный подтекст', value: '147' },
{ label: 'Комплекс брата', value: '105' },
{ label: 'Комплекс неполноценности', value: '365' },
{ label: 'Контракты', value: '155' },
{ label: 'Контроль разума', value: '441' },
{ label: 'Конфликт в семье', value: '261' },
{ label: 'Конфликт верности', value: '153' },
{ label: 'Копейщик', value: '668' },
{ label: 'Королевская особа', value: '596' },
{ label: 'Королевства', value: '382' },
{ label: 'Короткий рассказ', value: '638' },
{ label: 'Коррупция', value: '157' },
{ label: 'Космические Войны', value: '158' },
{ label: 'Космос', value: '507' },
{ label: 'Косплей', value: '159' },
{ label: 'Кража способностей', value: '2' },
{ label: 'Крафтинг', value: '164' },
{ label: 'Кризис личности', value: '357' },
{ label: 'Кровь', value: '320' },
{ label: 'Кружки, клубы', value: '138' },
{ label: 'Кудэрэ', value: '384' },
{ label: 'Кузнец', value: '90' },
{ label: 'Кукловоды', value: '567' },
{ label: 'Куклы/Марионетки', value: '211' },
{ label: 'Кулинария', value: '156' },
{ label: 'Культивация', value: '171' },
{ label: 'Куннилингус', value: '172' },
{ label: 'Легенды', value: '391' },
{ label: 'Легкая жизнь', value: '229' },
{ label: 'Лекари', value: '334' },
{ label: 'Ленивый главный герой', value: '389' },
{ label: 'Лидерство', value: '390' },
{ label: 'Любит персонажа младше себя', value: '765' },
{ label: 'Любит персонажа старше себя', value: '494' },
{ label: 'Любовное соперничество', value: '406' },
{ label: 'Любовные треугольники', value: '407' },
{ label: 'Любовь детства', value: '129' },
{ label: 'Любовь с первого взгляда', value: '404' },
{ label: 'Любящие родители', value: '215' },
{ label: 'Маги', value: '757' },
{ label: 'Магические технологии', value: '417' },
{ label: 'Магия', value: '412' },
{ label: 'Магия призыва', value: '694' },
{ label: 'Маленькие девочки', value: '397' },
{ label: 'Мангака', value: '423' },
{ label: 'Маниакальная любовь', value: '492' },
{ label: 'Марти Сью', value: '508' },
{ label: 'Мастер подземелий', value: '222' },
{ label: 'Мастурбация', value: '432' },
{ label: 'Матриархат', value: '433' },
{ label: 'Межпространственные путешествия', value: '370' },
{ label: 'Менеджмент', value: '422' },
{ label: 'Месть', value: '588' },
{ label: 'Мечи и магия', value: '697' },
{ label: 'Мечники', value: '698' },
{ label: 'Миленькие дети', value: '176' },
{ label: 'Милитаризм', value: '439' },
{ label: 'Миловидная история', value: '178' },
{ label: 'Милый главный герой', value: '177' },
{ label: 'Минет', value: '275' },
{ label: 'Мировое древо', value: '760' },
{ label: 'Миролюбивый главный герой', value: '510' },
{ label: 'Мифология', value: '475' },
{ label: 'Младшие сёстры', value: '766' },
{ label: 'ММОРПГ', value: '445' },
{ label: 'Множество временных линий', value: '463' },
{ label: 'Модели', value: '447' },
{ label: 'Молод не по годам', value: '49' },
{ label: 'Монстры', value: '454' },
{ label: 'Мрачность', value: '183' },
{ label: 'Музыка', value: '466' },
{ label: 'Музыкальные группы', value: '68' },
{ label: 'Мутации', value: '468' },
{ label: 'На волосок от смерти', value: '479' },
{ label: 'На все руки мастер', value: '374' },
{ label: 'Навыки имеют стоимость', value: '150' },
{ label: 'Нагота', value: '490' },
{ label: 'Наездники на драконах', value: '216' },
{ label: 'Наёмники', value: '437' },
{ label: 'Наивный главный герой', value: '476' },
{ label: 'Наследие', value: '366' },
{ label: 'Настоящая любовь', value: '200' },
{ label: 'Настоящий гарем', value: '540' },
{ label: 'Небесное испытание', value: '337' },
{ label: 'Невезучий главный герой', value: '738' },
{ label: 'Недоверчивый протагонист', value: '206' },
{ label: 'Недооцененный главный герой', value: '734' },
{ label: 'Недопонимание', value: '444' },
{ label: 'Некромант', value: '480' },
{ label: 'Нелинейное повествование', value: '489' },
{ label: 'Неловкий главный герой', value: '67' },
{ label: 'Нелюбимый протагонист', value: '333' },
{ label: 'Немой персонаж', value: '469' },
{ label: 'Ненадежный рассказчик', value: '739' },
{ label: 'Непримечательный герой', value: '65' },
{ label: 'Нерешительный протагонист', value: '363' },
{ label: 'Несгибаемый главный герой', value: '676' },
{ label: 'Несколько главных героев', value: '460' },
{ label: 'Несколько миров', value: '461' },
{ label: 'Несколько перерожденцев', value: '462' },
{ label: 'Несколько попаданцев', value: '464' },
{ label: 'Несравненная скорость культивации', value: '269' },
{ label: 'Нехватка/отсутствие здравого смысла', value: '385' },
{ label: 'Нечестный протагонист', value: '205' },
{ label: 'НИИТ', value: '481' },
{ label: 'Ниндзя', value: '486' },
{ label: 'Обмен телами', value: '95' },
{ label: 'Оборотни', value: '754' },
{ label: 'Объект любви влюбился первым', value: '405' },
{ label: 'Объект любви популярен', value: '543' },
{ label: 'Огнестрельное оружие', value: '280' },
{ label: 'Ограничение на время жизни', value: '394' },
{ label: 'Одержимость', value: '544' },
{ label: 'Один родитель', value: '651' },
{ label: 'Одинокий главный герой', value: '399' },
{ label: 'Одиночество', value: '398' },
{ label: 'Одно тело на двоих', value: '634' },
{ label: 'Око силы', value: '253' },
{ label: 'Оммёдзи', value: '498' },
{ label: 'Организованная преступность', value: '500' },
{ label: 'Оргия', value: '501' },
{ label: 'Орки', value: '499' },
{ label: 'Основано на аниме', value: '74' },
{ label: 'Основано на видеоигре', value: '72' },
{ label: 'Основано на визуальной новелле', value: '73' },
{ label: 'Основано на песне', value: '70' },
{ label: 'Основано на фильме', value: '69' },
{ label: 'Осторожный главный герой', value: '117' },
{ label: 'От ненависти до любви один шаг', value: '239' },
{ label: 'От сильного с сильнейшему', value: '684' },
{ label: 'От слабого к сильному', value: '752' },
{ label: 'Отаку', value: '503' },
{ label: 'Отзывчивая любовь', value: '380' },
{ label: 'Отзывчивый главный герой', value: '339' },
{ label: 'Отношение учитель-ученик', value: '430' },
{ label: 'Отношения бог-человек', value: '315' },
{ label: 'Отношения между учителем и учеником', value: '687' },
{ label: 'Отношения начальник-подчинённый', value: '101' },
{ label: 'Отношения против воли', value: '286' },
{ label: 'Отношения с нечеловеком', value: '353' },
{ label: 'Отношения сэмпай-кохай', value: '621' },
{ label: 'Отношения хозяин-слуга', value: '431' },
{ label: 'Отомэ-игры', value: '504' },
{ label: 'ОТП', value: '562' },
{ label: 'Отшельники', value: '505' },
{ label: 'Офисный работник', value: '600' },
{ label: 'Официанты', value: '748' },
{ label: 'Охотники', value: '355' },
{ label: 'Падшие ангелы', value: '255' },
{ label: 'Падшие дворяне', value: '256' },
{ label: 'Пайзури', value: '511' },
{ label: 'Паразиты', value: '513' },
{ label: 'Параллельные миры', value: '512' },
{ label: 'Парк развлечений', value: '32' },
{ label: 'Пародия', value: '515' },
{ label: 'Перемещение между мирами', value: '758' },
{ label: 'Переодевание', value: '167' },
{ label: 'Переплавка пилюль', value: '529' },
{ label: 'Перерождение в девушку', value: '420' },
{ label: 'Перерождение в другом мире', value: '580' },
{ label: 'Перерождение в другом мире', value: '720' },
{ label: 'Перерождение в игровом мире', value: '579' },
{ label: 'Перерождение в монстра', value: '577' },
{ label: 'Персонажи с мазохистскими наклонностями', value: '429' },
{ label: 'Персонажи с садистскими наклонностями', value: '598' },
{ label: 'Петля времени', value: '709' },
{ label: 'Петтинг', value: '328' },
{ label: 'Писатели', value: '761' },
{ label: 'Питомцы', value: '522' },
{ label: 'Племенное общество', value: '726' },
{ label: 'Повелитель демонов', value: '191' },
{ label: 'Повествование от нескольких лиц', value: '459' },
{ label: 'Поглощение навыков', value: '653' },
{ label: 'Подземелья', value: '223' },
{ label: 'Поздно начинающаяся романтическая линия', value: '387' },
{ label: 'Политика', value: '538' },
{ label: 'Полулюди', value: '190' },
{ label: 'Помолвка', value: '240' },
{ label: 'Попаданец в другой мир', value: '723' },
{ label: 'Попаданец в игровой мир', value: '722' },
{ label: 'Постапокалипсис', value: '546' },
{ label: 'Постижение Дао', value: '181' },
{ label: 'Потеря девственности', value: '282' },
{ label: 'Потустороннее пространство', value: '416' },
{ label: 'Похищения', value: '379' },
{ label: 'Прагматичный главный герой', value: '549' },
{ label: 'Предательство', value: '83' },
{ label: 'Предвидение', value: '550' },
{ label: 'Преследователи', value: '674' },
{ label: 'Преступление', value: '165' },
{ label: 'Преступность', value: '166' },
{ label: 'Приёмные дети', value: '19' },
{ label: 'Призванный герой', value: '693' },
{ label: 'Призраки', value: '309' },
{ label: 'Приключения', value: '22' },
{ label: 'Принуждение', value: '108' },
{ label: 'Приручатель', value: '453' },
{ label: 'Прислуга', value: '625' },
{ label: 'Приставучий любовник', value: '136' },
{ label: 'Притворная любовь', value: '552' },
{ label: 'Пришельцы', value: '28' },
{ label: 'Программист', value: '558' },
{ label: 'Произведение - обладатель наград', value: '66' },
{ label: 'Проклятия', value: '175' },
{ label: 'Промывка мозгов', value: '102' },
{ label: 'Пропуск времени', value: '712' },
{ label: 'Пророчества', value: '559' },
{ label: 'Протагонист в очках', value: '312' },
{ label: 'Протагонист раб', value: '657' },
{ label: 'Психокинез', value: '565' },
{ label: 'Психопаты', value: '566' },
{ label: 'Путешествия', value: '759' },
{ label: 'Путешествия во времени', value: '713' },
{ label: 'Пытки', value: '716' },
{ label: 'Рабы', value: '658' },
{ label: 'Развитие отношений', value: '160' },
{ label: 'Развитие персонажа', value: '119' },
{ label: 'Раздвоение личности', value: '458' },
{ label: 'Размеренная романтика', value: '661' },
{ label: 'Разница в статусе', value: '201' },
{ label: 'Разорванная помолвка', value: '104' },
{ label: 'Разумные предметы', value: '622' },
{ label: 'Рай', value: '336' },
{ label: 'Раняя романтика', value: '227' },
{ label: 'Расизм', value: '573' },
{ label: 'Расследования', value: '372' },
{ label: 'Ребенок — главный герой', value: '126' },
{ label: 'Реверс-гарем', value: '589' },
{ label: 'Реверс-изнасилование', value: '590' },
{ label: 'Резкая смена характера', value: '520' },
{ label: 'Реинкарнация', value: '581' },
{ label: 'Религии', value: '582' },
{ label: 'Ресторан', value: '585' },
{ label: 'Решительный главный герой', value: '199' },
{ label: 'Робкий протагонист', value: '714' },
{ label: 'Родительсткий комплекс', value: '514' },
{ label: 'Родные не связанные кровью', value: '647' },
{ label: 'Родословные', value: '94' },
{ label: 'РПГ-система', value: '392' },
{ label: 'Рыцари', value: '383' },
{ label: 'Самовлюбленный главный герой', value: '477' },
{ label: 'Самоубийства', value: '692' },
{ label: 'Самураи', value: '601' },
{ label: 'Сборник коротких историй', value: '144' },
{ label: 'Свадьба', value: '426' },
{ label: 'Связанные сюжетные линии', value: '369' },
{ label: 'Святые', value: '599' },
{ label: 'Секреты', value: '614' },
{ label: 'Секс втроём', value: '707' },
{ label: 'Секс-рабыня', value: '629' },
{ label: 'Семейная любовь', value: '257' },
{ label: 'Семь добродетелей', value: '627' },
{ label: 'Семь смертных грехов', value: '626' },
{ label: 'Серийные убийцы', value: '624' },
{ label: 'Сикигами', value: '637' },
{ label: 'Сильная любовь от старших', value: '214' },
{ label: 'Синдром восьмиклассника', value: '132' },
{ label: 'Синдром сестры', value: '652' },
{ label: 'Сироты', value: '502' },
{ label: 'Системный администратор', value: '699' },
{ label: 'Скромный главный герой', value: '409' },
{ label: 'Скрытые романтические отношения', value: '23' },
{ label: 'Скрытые способности', value: '343' },
{ label: 'Скряга', value: '450' },
{ label: 'Скульпторы', value: '606' },
{ label: 'Слабая романтика', value: '688' },
{ label: 'Слабый главный герой', value: '751' },
{ label: 'Сложные семейные отношения', value: '149' },
{ label: 'Смена расы', value: '572' },
{ label: 'Смертельное заболевание', value: '704' },
{ label: 'Смерть', value: '185' },
{ label: 'Смерть любимых', value: '186' },
{ label: 'Современность', value: '448' },
{ label: 'Современные знания в слаборазвитых мирах', value: '449' },
{ label: 'Сожительство', value: '141' },
{ label: 'Создание артефактов', value: '57' },
{ label: 'Создание клана', value: '133' },
{ label: 'Создание королевства', value: '381' },
{ label: 'Создание навыков', value: '655' },
{ label: 'Сокрытие истинных способностей', value: '344' },
{ label: 'Сокрытие личности', value: '345' },
{ label: 'Солдаты', value: '664' },
{ label: 'Сон', value: '659' },
{ label: 'Соседи по комнате', value: '595' },
{ label: 'Социальные изгои', value: '663' },
{ label: 'Спасение мира', value: '602' },
{ label: 'Спорящая пара', value: '84' },
{ label: 'Способность перевоплощения', value: '719' },
{ label: 'Средневековье', value: '436' },
{ label: 'Стеснительные персонажи', value: '643' },
{ label: 'Стокгольмский синдром', value: '575' },
{ label: 'Стокгольмский синдром', value: '675' },
{ label: 'Стратег', value: '681' },
{ label: 'Стратегические битвы', value: '680' },
{ label: 'Стрелки', value: '325' },
{ label: 'Стрельба из лука', value: '50' },
{ label: 'Студенческий совет', value: '686' },
{ label: 'Судьба', value: '197' },
{ label: 'Суккубы', value: '689' },
{ label: 'Суперспособности', value: '669' },
{ label: 'Суровая тренировка', value: '332' },
{ label: 'Сцены насилия', value: '195' },
{ label: 'Таинственное прошлое', value: '472' },
{ label: 'Тайная личность', value: '610' },
{ label: 'Тайные организации', value: '611' },
{ label: 'Телохранители', value: '98' },
{ label: 'Тентакли', value: '703' },
{ label: 'Террористы', value: '705' },
{ label: 'Технологический разрыв', value: '702' },
{ label: 'Тихие герои', value: '568' },
{ label: 'Толстый протагонист', value: '271' },
{ label: 'Торговцы', value: '438' },
{ label: 'Травник', value: '340' },
{ label: 'Трагичное прошлое', value: '718' },
{ label: 'Трап', value: '725' },
{ label: 'Триллер', value: '708' },
{ label: 'Турнир', value: '76' },
{ label: 'Тюрьма', value: '556' },
{ label: 'Убийства', value: '465' },
{ label: 'Уверенный в себе главный герой', value: '151' },
{ label: 'Умная пара', value: '662' },
{ label: 'Умный главный герой', value: '135' },
{ label: 'Уникальная техника культивации', value: '735' },
{ label: 'Уникальные оружия', value: '737' },
{ label: 'Управление бизнесом', value: '109' },
{ label: 'Управление временем', value: '710' },
{ label: 'Управление пространством', value: '667' },
{ label: 'Ускоренный рост', value: '6' },
{ label: 'Учёные', value: '605' },
{ label: 'Учителя', value: '700' },
{ label: 'Фамильяры', value: '258' },
{ label: 'Фанатизм', value: '264' },
{ label: 'Фармацевт', value: '523' },
{ label: 'Феи', value: '254' },
{ label: 'Фениксы', value: '526' },
{ label: 'Фетиш на грудь', value: '103' },
{ label: 'Фетиш на очки', value: '311' },
{ label: 'Философия', value: '524' },
{ label: 'Фобии/Страхи', value: '525' },
{ label: 'Фольклор', value: '285' },
{ label: 'Формирование армии', value: '54' },
{ label: 'Футанари', value: '295' },
{ label: 'Футуристический мир', value: '296' },
{ label: 'Фэнтези', value: '267' },
{ label: 'Фэнтезийные существа', value: '266' },
{ label: 'Хакеры', value: '326' },
{ label: 'Харизматичный протагонист', value: '120' },
{ label: 'Хитрый главный герой', value: '173' },
{ label: 'Хладнокровный главный герой', value: '112' },
{ label: 'Художники', value: '60' },
{ label: 'Хулиганы', value: '188' },
{ label: 'Цундере', value: '728' },
{ label: 'Чат', value: '122' },
{ label: 'Человек-оружие', value: '352' },
{ label: 'Человекоподобный главный герой', value: '354' },
{ label: 'Честный главный герой', value: '348' },
{ label: 'Четкие любовные убеждения', value: '683' },
{ label: 'Читы', value: '123' },
{ label: 'Чудаковатые герои', value: '569' },
{ label: 'Чужие воспоминания', value: '721' },
{ label: 'Шантаж', value: '89' },
{ label: 'Шеф-повар', value: '124' },
{ label: 'Школа для девочек', value: '29' },
{ label: 'Шоу-бизнес', value: '642' },
{ label: 'Шпионы', value: '670' },
{ label: 'Эволюция', value: '250' },
{ label: 'Эгоистичный главный герой', value: '618' },
{ label: 'Экзорцизм', value: '252' },
{ label: 'Экономика', value: '230' },
{ label: 'Эксгибиционизм', value: '251' },
{ label: 'Эксперименты на людях', value: '351' },
{ label: 'Элементальная магия', value: '234' },
{ label: 'Элементы игрового мира', value: '299' },
{ label: 'Элементы романтики', value: '594' },
{ label: 'Элементы юри', value: '640' },
{ label: 'Элементы яоя', value: '641' },
{ label: 'Эльфы', value: '235' },
{ label: 'Эпизодичность', value: '243' },
{ label: 'Язвительные персонажи', value: '635' },
{ label: 'Языковой барьер', value: '386' },
{ label: 'Яндере', value: '762' },
{ label: 'Яойщица', value: '294' },
{ label: 'Японские силы самообороны', value: '378' },
{ label: 'R-15', value: '570' },
{ label: 'R-18', value: '571' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: 'Жанры',
value: [],
options: [
{ label: '16+', value: '1' },
{ label: '18+', value: '2' },
{ label: 'Боевик', value: '3' },
{ label: 'Боевые искусства', value: '4' },
{ label: 'Вампиры', value: '47' },
{ label: 'Военное', value: '44' },
{ label: 'Гарем', value: '5' },
{ label: 'Демоны', value: '48' },
{ label: 'Детектив', value: '6' },
{ label: 'Дзёсей', value: '50' },
{ label: 'Драма', value: '7' },
{ label: 'Игры', value: '45' },
{ label: 'Историческое', value: '8' },
{ label: 'Киберпанк', value: '9' },
{ label: 'Комедия', value: '10' },
{ label: 'Космос', value: '46' },
{ label: 'Магия', value: '42' },
{ label: 'Махо-сёдзё', value: '12' },
{ label: 'Меха', value: '13' },
{ label: 'Мистика', value: '14' },
{ label: 'Музыка', value: '49' },
{ label: 'Научная фантастика', value: '15' },
{ label: 'Пародия', value: '16' },
{ label: 'Повседневность', value: '17' },
{ label: 'Приключения', value: '18' },
{ label: 'Психологическое', value: '19' },
{ label: 'Романтика', value: '20' },
{ label: 'Сверхъестественное', value: '21' },
{ label: 'Сёдзё', value: '22' },
{ label: 'Сёдзё-ай', value: '23' },
{ label: 'Сёнэн', value: '24' },
{ label: 'Сёнэн-ай', value: '25' },
{ label: 'Смена пола', value: '26' },
{ label: 'Современность', value: '27' },
{ label: 'Спорт', value: '28' },
{ label: 'Супер сила', value: '43' },
{ label: 'Сэйнэн', value: '29' },
{ label: 'Трагедия', value: '32' },
{ label: 'Триллер', value: '33' },
{ label: 'Ужасы', value: '34' },
{ label: 'Уся', value: '35' },
{ label: 'Фэнтези', value: '36' },
{ label: 'Школьная жизнь', value: '37' },
{ label: 'Этти', value: '38' },
{ label: 'Юри', value: '39' },
{ label: 'Яой', value: '40' },
{ label: 'LitRPG', value: '41' },
],
type: FilterTypes.CheckboxGroup,
},
} satisfies Filters;
}
export default new TL();
interface response {
data: Data;
}
interface Data {
projects?: Projects;
project?: Project;
chapter?: DataChapter;
}
interface DataChapter {
text: Text;
}
interface Text {
text: string;
}
interface Project {
title: string;
translationStatus: string;
fullUrl: string;
covers: Cover[];
persons: Person[];
genres: Genre[];
tags: Genre[];
annotation: Text;
subprojects: Subprojects;
}
interface Cover {
url: string;
}
interface Genre {
nameRu: string;
nameEng: string;
}
interface Person {
role: string;
name: Name;
}
interface Name {
firstName: string;
lastName: string;
}
interface Subprojects {
content: SubprojectsContent[];
}
interface SubprojectsContent {
title: null | string;
volumes: Volumes;
}
interface Volumes {
content: VolumesContent[];
}
interface VolumesContent {
shortName: null | string;
chapters: ChapterElement[];
}
interface ChapterElement {
title: null | string;
publishDate: Date;
fullUrl: string;
published: boolean;
}
interface Projects {
content: ProjectsContent[];
}
interface ProjectsContent {
title: string;
fullUrl: string;
covers: Cover[];
}
File diff suppressed because it is too large Load Diff
-726
View File
@@ -1,726 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { storage, localStorage } from '@libs/storage';
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';
apiSite = 'https://api.lib.social/api/manga/';
version = '2.0.1';
icon = 'src/ru/ranobelib/icon.png';
webStorageUtilized = true;
async popularNovels(
pageNo: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.apiSite + '?site_id[0]=3&page=' + pageNo;
url +=
'&sort_by=' +
(showLatestNovels
? 'last_chapter_at'
: filters?.sort_by?.value || 'rating_score');
url += '&sort_type=' + (filters?.sort_type?.value || 'desc');
if (filters?.require_chapters?.value) {
url += '&chapters[min]=1';
}
if (filters?.types?.value?.length) {
url += '&types[]=' + filters.types.value.join('&types[]=');
}
if (filters?.scanlateStatus?.value?.length) {
url +=
'&scanlateStatus[]=' +
filters.scanlateStatus.value.join('&scanlateStatus[]=');
}
if (filters?.manga_status?.value?.length) {
url +=
'&manga_status[]=' +
filters.manga_status.value.join('&manga_status[]=');
}
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: TopLevel = await fetchApi(url, {
headers: this.user?.token,
}).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (result.data instanceof Array) {
result.data.forEach(novel =>
novels.push({
name: novel.rus_name || novel.eng_name || novel.name,
cover: novel.cover?.default || defaultCover,
path: novel.slug_url || novel.id + '--' + novel.slug,
}),
);
}
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const { data }: { data: DataClass } = await fetchApi(
this.apiSite +
novelPath +
'?fields[]=summary&fields[]=genres&fields[]=tags&fields[]=teams&fields[]=authors&fields[]=status_id&fields[]=artists',
{ headers: this.user?.token },
).then(res => res.json());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: data.rus_name || data.name,
cover: data.cover?.default || defaultCover,
summary: data.summary,
};
if (data.status?.id) {
novel.status = statusKey[data.status.id] || NovelStatus.Unknown;
}
if (data.authors?.length) {
novel.author = data.authors[0].name;
}
if (data.artists?.length) {
novel.artist = data.artists[0].name;
}
const genres = [data.genres || [], data.tags || []]
.flat()
.map(genres => genres?.name)
.filter(genres => genres);
if (genres.length) {
novel.genres = genres.join(', ');
}
const branch_id: { [key: number]: string } = {};
if (data.teams.length) {
data.teams.forEach(
({ name, details }) => (branch_id[details?.branch_id || '0'] = name),
);
}
const chaptersJSON: { data: DataChapter[] } = await fetchApi(
this.apiSite + novelPath + '/chapters',
{
headers: this.user?.token,
},
).then(res => res.json());
if (chaptersJSON.data.length) {
const chapters: Plugin.ChapterItem[] = [];
chaptersJSON.data.forEach(chapter =>
chapters.push({
name:
'Том ' +
chapter.volume +
' Глава ' +
chapter.number +
(chapter.name ? ' ' + chapter.name : ''),
path:
novelPath +
'/' +
chapter.volume +
'/' +
chapter.number +
'/' +
(chapter.branches[0]?.branch_id || ''),
releaseTime: dayjs(chapter.branches[0].created_at).format('LLL'),
chapterNumber: chapter.index,
page: branch_id[chapter.branches[0].branch_id || '0'],
}),
);
novel.chapters = chapters;
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const [slug, volume, number, branch_id] = chapterPath.split('/');
let chapterText = '';
if (slug && volume && number) {
const result: { data: DataClass } = await fetchApi(
this.apiSite +
slug +
'/chapter?' +
(branch_id ? 'branch_id=' + branch_id + '&' : '') +
'number=' +
number +
'&volume=' +
volume,
{ headers: this.user?.token },
).then(res => res.json());
chapterText =
result?.data?.content?.type == 'doc'
? jsonToHtml(result.data.content.content)
: result?.data?.content;
}
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = this.apiSite + '?site_id[0]=3&q=' + searchTerm;
const result: TopLevel = await fetchApi(url, {
headers: this.user?.token,
}).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (result.data instanceof Array) {
result.data.forEach(novel =>
novels.push({
name: novel.rus_name || novel.eng_name || novel.name,
cover: novel.cover?.default || defaultCover,
path: novel.slug_url || novel.id + '--' + novel.slug,
}),
);
}
return novels;
}
resolveUrl = (path: string, isNovel?: boolean) => {
const ui = this.user?.ui ? 'ui=' + this.user.ui : '';
if (isNovel) return this.site + '/ru/book/' + path + (ui ? '?' + ui : '');
const [slug, volume, number, branch_id] = path.split('/');
const chapterPath =
slug +
'/read/v' +
volume +
'/c' +
number +
(branch_id ? '?bid=' + branch_id : '');
return (
this.site +
'/ru/' +
chapterPath +
(ui ? (branch_id ? '&' : '?') + ui : '')
);
};
getUser = () => {
const user = storage.get('user');
if (user) {
return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id };
}
const dataRaw = localStorage.get()?.auth;
if (!dataRaw) {
return {};
}
const data = JSON.parse(dataRaw) as authorization;
if (!data?.token?.access_token) return;
storage.set(
'user',
{
id: data.auth.id,
token: data.token.access_token,
},
data.token.timestamp + data.token.expires_in, //the token is valid for about 7 days
);
return {
token: { Authorization: 'Bearer ' + data.token.access_token },
ui: data.auth.id,
};
};
user = this.getUser(); //To change the account, you need to restart the application
filters = {
sort_by: {
label: 'Сортировка',
value: 'rating_score',
options: [
{ label: 'По рейтингу', value: 'rate_avg' },
{ label: 'По популярности', value: 'rating_score' },
{ label: 'По просмотрам', value: 'views' },
{ label: 'Количеству глав', value: 'chap_count' },
{ label: 'Дате обновления', value: 'last_chapter_at' },
{ label: 'Дате добавления', value: 'created_at' },
{ label: 'По названию (A-Z)', value: 'name' },
{ label: 'По названию (А-Я)', value: 'rus_name' },
],
type: FilterTypes.Picker,
},
sort_type: {
label: 'Порядок',
value: 'desc',
options: [
{ label: 'По убыванию', value: 'desc' },
{ label: 'По возрастанию', value: 'asc' },
],
type: FilterTypes.Picker,
},
types: {
label: 'Тип',
value: [],
options: [
{ label: 'Япония', value: '10' },
{ label: 'Корея', value: '11' },
{ label: 'Китай', value: '12' },
{ label: 'Английский', value: '13' },
{ label: 'Авторский', value: '14' },
{ label: 'Фанфик', value: '15' },
],
type: FilterTypes.CheckboxGroup,
},
scanlateStatus: {
label: 'Статус перевода',
value: [],
options: [
{ label: 'Продолжается', value: '1' },
{ label: 'Завершен', value: '2' },
{ label: 'Заморожен', value: '3' },
{ label: 'Заброшен', value: '4' },
],
type: FilterTypes.CheckboxGroup,
},
manga_status: {
label: 'Статус тайтла',
value: [],
options: [
{ label: 'Онгоинг', value: '1' },
{ label: 'Завершён', value: '2' },
{ label: 'Анонс', value: '3' },
{ label: 'Приостановлен', value: '4' },
{ label: 'Выпуск прекращён', value: '5' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: 'Жанры',
value: { include: [], exclude: [] },
options: [
{ label: 'Арт', value: '32' },
{ label: 'Безумие', value: '91' },
{ label: 'Боевик', value: '34' },
{ label: 'Боевые искусства', value: '35' },
{ label: 'Вампиры', value: '36' },
{ label: 'Военное', value: '89' },
{ label: 'Гарем', value: '37' },
{ label: 'Гендерная интрига', value: '38' },
{ label: 'Героическое фэнтези', value: '39' },
{ label: 'Демоны', value: '81' },
{ label: 'Детектив', value: '40' },
{ label: 'Детское', value: '88' },
{ label: 'Дзёсэй', value: '41' },
{ label: 'Драма', value: '43' },
{ label: 'Игра', value: '44' },
{ label: 'Исекай', value: '79' },
{ label: 'История', value: '45' },
{ label: 'Киберпанк', value: '46' },
{ label: 'Кодомо', value: '76' },
{ label: 'Комедия', value: '47' },
{ label: 'Космос', value: '83' },
{ label: 'Магия', value: '85' },
{ label: 'Махо-сёдзё', value: '48' },
{ label: 'Машины', value: '90' },
{ label: 'Меха', value: '49' },
{ label: 'Мистика', value: '50' },
{ label: 'Музыка', value: '80' },
{ label: 'Научная фантастика', value: '51' },
{ label: 'Омегаверс', value: '77' },
{ label: 'Пародия', value: '86' },
{ label: 'Повседневность', value: '52' },
{ label: 'Полиция', value: '82' },
{ label: 'Постапокалиптика', value: '53' },
{ label: 'Приключения', value: '54' },
{ label: 'Психология', value: '55' },
{ label: 'Романтика', value: '56' },
{ label: 'Самурайский боевик', value: '57' },
{ label: 'Сверхъестественное', value: '58' },
{ label: 'Сёдзё', value: '59' },
{ label: 'Сёдзё-ай', value: '60' },
{ label: 'Сёнэн', value: '61' },
{ label: 'Сёнэн-ай', value: '62' },
{ label: 'Спорт', value: '63' },
{ label: 'Супер сила', value: '87' },
{ label: 'Сэйнэн', value: '64' },
{ label: 'Трагедия', value: '65' },
{ label: 'Триллер', value: '66' },
{ label: 'Ужасы', value: '67' },
{ label: 'Фантастика', value: '68' },
{ label: 'Фэнтези', value: '69' },
{ label: 'Хентай', value: '84' },
{ label: 'Школа', value: '70' },
{ label: 'Эротика', value: '71' },
{ label: 'Этти', value: '72' },
{ label: 'Юри', value: '73' },
{ label: 'Яой', value: '74' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
tags: {
label: 'Теги',
value: { include: [], exclude: [] },
options: [
{ label: 'Авантюристы', value: '328' },
{ label: 'Антигерой', value: '175' },
{ label: 'Бессмертные', value: '333' },
{ label: 'Боги', value: '218' },
{ label: 'Борьба за власть', value: '309' },
{ label: 'Брат и сестра', value: '360' },
{ label: 'Ведьма', value: '339' },
{ label: 'Видеоигры', value: '204' },
{ label: 'Виртуальная реальность', value: '214' },
{ label: 'Владыка демонов', value: '349' },
{ label: 'Военные', value: '198' },
{ label: 'Воспоминания из другого мира', value: '310' },
{ label: 'Выживание', value: '212' },
{ label: 'ГГ женщина', value: '294' },
{ label: 'ГГ имба', value: '292' },
{ label: 'ГГ мужчина', value: '295' },
{ label: 'ГГ не ояш', value: '325' },
{ label: 'ГГ не человек', value: '331' },
{ label: 'ГГ ояш', value: '326' },
{ label: 'Главный герой бог', value: '324' },
{ label: 'Глупый ГГ', value: '298' },
{ label: 'Горничные', value: '171' },
{ label: 'Гуро', value: '306' },
{ label: 'Гяру', value: '197' },
{ label: 'Демоны', value: '157' },
{ label: 'Драконы', value: '313' },
{ label: 'Древний мир', value: '317' },
{ label: 'Зверолюди', value: '163' },
{ label: 'Зомби', value: '155' },
{ label: 'Исторические фигуры', value: '323' },
{ label: 'Кулинария', value: '158' },
{ label: 'Культивация', value: '161' },
{ label: 'ЛГБТ', value: '344' },
{ label: 'ЛитРПГ', value: '319' },
{ label: 'Лоли', value: '206' },
{ label: 'Магия', value: '170' },
{ label: 'Машинный перевод', value: '345' },
{ label: 'Медицина', value: '159' },
{ label: 'Межгалактическая война', value: '330' },
{ label: 'Монстр Девушки', value: '207' },
{ label: 'Монстры', value: '208' },
{ label: 'Мрачный мир', value: '316' },
{ label: 'Музыка', value: '358' },
{ label: 'Музыка', value: '209' },
{ label: 'Ниндзя', value: '199' },
{ label: 'Обратный Гарем', value: '210' },
{ label: 'Офисные Работники', value: '200' },
{ label: 'Пираты', value: '341' },
{ label: 'Подземелья', value: '314' },
{ label: 'Политика', value: '311' },
{ label: 'Полиция', value: '201' },
{ label: 'Преступники / Криминал', value: '205' },
{ label: 'Призраки / Духи', value: '196' },
{ label: 'Призыватели', value: '329' },
{ label: 'Прыжки между мирами', value: '321' },
{ label: 'Путешествие в другой мир', value: '318' },
{ label: 'Путешествие во времени', value: '213' },
{ label: 'Рабы', value: '355' },
{ label: 'Ранги силы', value: '312' },
{ label: 'Реинкарнация', value: '154' },
{ label: 'Самураи', value: '202' },
{ label: 'Скрытие личности', value: '315' },
{ label: 'Средневековье', value: '174' },
{ label: 'Традиционные игры', value: '203' },
{ label: 'Умный ГГ', value: '303' },
{ label: 'Характерный рост', value: '332' },
{ label: 'Хикикомори', value: '167' },
{ label: 'Эволюция', value: '322' },
{ label: 'Элементы РПГ', value: '327' },
{ label: 'Эльфы', value: '217' },
{ label: 'Якудза', value: '165' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
require_chapters: {
label: 'Только проекты с главами',
value: true,
type: FilterTypes.Switch,
},
} satisfies Filters;
}
export default new RLIB();
function jsonToHtml(json: HTML[], html: string = '') {
json.forEach(element => {
switch (element.type) {
case 'hardBreak':
html += '<br>';
break;
case 'horizontalRule':
html += '<hr>';
break;
case 'image':
if (element.attrs) {
const attrs = Object.entries(element.attrs)
.filter(attr => attr?.[1])
.map(attr => `${attr[0]}="${attr[1]}"`);
html += '<img ' + attrs.join('; ') + '>';
}
break;
case 'paragraph':
html +=
'<p>' +
(element.content ? jsonToHtml(element.content) : '<br>') +
'</p>';
break;
case 'heading':
html +=
'<h2>' +
(element.content ? jsonToHtml(element.content) : '<br>') +
'</h2>';
break;
case 'text':
html += element.text;
break;
default:
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
break;
}
});
return html;
}
interface HTML {
type: string;
content?: HTML[];
attrs?: Attrs;
text?: string;
}
interface Attrs {
src: string;
alt: string | null;
title: string | null;
}
interface authorization {
token: Token;
auth: Auth;
timestamp: number;
}
interface Token {
token_type: string;
expires_in: number;
access_token: string;
refresh_token: string;
timestamp: number;
}
interface Auth {
id: number;
username: string;
avatar: Cover;
last_online_at: string;
metadata: Metadata;
}
interface Metadata {
auth_domains: string;
}
interface TopLevel {
data: DataClass | DataClass[];
links?: Links;
meta?: Meta;
}
interface AgeRestriction {
id: number;
label: string;
}
interface Branch {
id: number;
branch_id: null;
created_at: string;
teams: BranchTeam[];
user: User;
}
interface BranchTeam {
id: number;
slug: string;
slug_url: string;
model: string;
name: string;
cover: Cover;
}
interface Cover {
filename: null | string;
thumbnail: string;
default: string;
}
interface User {
username: string;
id: number;
}
interface Rating {
average: string;
votes: number;
votesFormated: string;
}
interface DataClass {
id: number;
name: string;
rus_name?: string;
eng_name?: string;
slug: string;
slug_url?: string;
cover?: Cover;
ageRestriction?: AgeRestriction;
site?: number;
type: string;
summary?: string;
is_licensed?: boolean;
teams: DataTeam[];
genres?: Genre[];
tags?: Genre[];
authors?: Artist[];
model?: string;
status?: AgeRestriction;
scanlateStatus?: AgeRestriction;
artists?: Artist[];
releaseDateString?: string;
volume?: string;
number?: string;
number_secondary?: string;
branch_id?: null;
manga_id?: number;
created_at?: string;
moderated?: AgeRestriction;
likes_count?: number;
content?: any;
attachments?: Attachment[];
}
interface Artist {
id: number;
slug: string;
slug_url: string;
model: string;
name: string;
rus_name: null;
alt_name: null;
cover: Cover;
subscription: Subscription;
confirmed: null;
user_id: number;
titles_count_details: null;
}
interface Subscription {
is_subscribed: boolean;
source_type: string;
source_id: number;
relation: null;
}
interface Attachment {
id: null;
filename: string;
name: string;
extension: string;
url: string;
width: number;
height: number;
}
interface Genre {
id: number;
name: string;
}
interface DataTeam {
id: number;
slug: string;
slug_url: string;
model: string;
name: string;
cover: Cover;
details?: Details;
vk?: string;
discord?: null;
}
interface Details {
branch_id: null;
is_active: boolean;
subscriptions_count: null;
}
interface Links {
first: string;
last: null;
prev: null;
next: string;
}
interface Meta {
current_page?: number;
from?: number;
path?: string;
per_page?: number;
to?: number;
page?: number;
has_next_page?: boolean;
seed?: string;
country?: string;
}
interface DataChapter {
id: number;
index: number;
item_number: number;
volume: string;
number: string;
number_secondary: string;
name: string;
branches_count: number;
branches: Branch[];
}
interface BranchesEntity {
id: number;
branch_id?: number;
created_at: string;
teams?: BranchTeam[];
user: User;
}
-413
View File
@@ -1,413 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
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.1';
icon = 'src/ru/ranoberf/icon.png';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/books?order=';
url += showLatestNovels
? 'lastPublishedChapter'
: filters?.sort?.value || 'popular';
url += '&page=' + pageNo;
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,
cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: '/' + novel.slug,
}),
);
}
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const novel: Plugin.SourceNovel = {
path: novelPath,
name: '',
};
const jsonRaw = body.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
const book: PagePropsBook = JSON.parse(jsonRaw[1]).props.pageProps.book;
novel.name = book.title;
novel.summary = book.description;
novel.cover = book.verticalImage?.url
? this.site + book.verticalImage.url
: defaultCover;
novel.status = book?.additionalInfo.includes('Активен')
? NovelStatus.Ongoing
: NovelStatus.Completed;
if (book.author) novel.author = book.author;
if (book.genres?.length)
novel.genres = book?.genres.map(item => item.title).join(',');
if (book.chapters?.length) {
const chapters: Plugin.ChapterItem[] = [];
book.chapters.forEach((chapter, chapterIndex) => {
if (!chapter.isDonate || chapter.isUserPaid) {
chapters.push({
name: chapter.title,
path: chapter.url,
releaseTime: dayjs(chapter.publishedAt).format('LLL'),
chapterNumber: book.chapters.length - chapterIndex,
});
}
});
novel.chapters = chapters.reverse();
}
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const jsonRaw = body.match(regex);
if (jsonRaw instanceof Array && jsonRaw[1]) {
let chapter: string =
JSON.parse(jsonRaw[1])?.props?.pageProps?.chapter?.content?.text || '';
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;
}
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 { items }: { items: Item[] } = await fetchApi(url).then(res =>
res.json(),
);
const novels: Plugin.NovelItem[] = [];
items.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: '/' + novel.slug,
}),
);
return novels;
}
filters = {
sort: {
label: 'Сортировка',
value: 'popular',
options: [
{ label: 'Рейтинг', value: 'popular' },
{ label: 'Дате добавления', value: 'new' },
{ label: 'Дате обновления', value: 'lastPublishedChapter' },
{ label: 'Законченные', value: 'completed' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new RNRF();
interface response {
props: Props;
page: string;
query: Query;
buildId: string;
isFallback: boolean;
gssp: boolean;
appGip: boolean;
dynamicIds?: string[];
}
interface Props {
pageProps: PageProps;
isMobile: boolean;
theme: string;
__N_SSP: boolean;
}
interface PageProps {
initialReduxState: InitialReduxState;
book?: PagePropsBook;
totalData?: TotalData;
layoutViewCookies?: string;
displayBanner?: boolean;
chapter?: PagePropsChapter;
}
interface PagePropsBook {
id: number;
countryId: number;
creatorId: number;
lastChapterId: number;
sourceId: number;
imgVerticalId: number;
imgHorizontalId: number;
imgSquareId: number;
imageVerticalId: number;
imageHorizontalId: number;
title: string;
titleEn: string;
fullTitle: string;
fullTitleEn: string;
description: string;
slug: string;
author: string;
additionalInfo: string;
importantInfo: string;
status: string;
views: number;
likes: number;
dislikes: number;
intervalSec: number;
intervalForNextFreeChapterSec: number;
chapterCost: number;
chapterPrice: number;
chapterPriceCollected: number;
maxSubscriptionChapters: number;
maxDonateChapters: number;
isDisplayOnMainPage: boolean;
withoutSchedule: boolean;
nextFreeChapterPublishedAt: null;
createdAt: Date;
updatedAt: Date;
userRating: null;
userBookmark: null;
country: Country;
genres: Genre[];
timer: Timer;
chapters: ChapterElement[];
verticalImage: Image;
horizontalImage: Image;
squareImage: Image;
}
interface ChapterElement {
id: number;
bookId: number;
title: string;
url: string;
numberChapter: string;
tom: number | null;
chapterShortNumber: number | null;
isDonate: boolean;
isSponsored: boolean;
isSubscription: boolean;
isEdited: boolean;
publishedAt: Date;
price: number;
views: number;
isUserPaid?: boolean;
}
interface Country {
id: number;
title: string;
code: string;
}
interface Genre {
id: number;
title: string;
slug: string;
}
interface Image {
id: number;
alt: string;
path: string;
name: string;
url: string;
}
interface Timer {
value: null;
for: null;
message: string;
}
interface PagePropsChapter {
id: number;
bookId: number;
editorId: null;
translatorId: number;
parserChapterId: null;
title: string;
slug: string;
status: string;
tom: null;
views: number;
isSponsored: boolean;
isSubscription: boolean;
isDonate: boolean;
isIndexing: boolean;
publishedAt: Date;
changeAvailabilityAt: null;
createdAt: Date;
updatedAt: Date;
numberChapter: string;
price: number;
content: Content;
book: ChapterBook;
userBookmark: null;
isUserPaid: boolean;
nextChapter: null;
previousChapter: PreviousChapter;
corrections: null;
}
interface ChapterBook {
id: number;
title: string;
titleEn: string;
url: string;
}
interface Content {
id: number;
text: string;
symbolsCount: number;
}
interface PreviousChapter {
id: number;
title: string;
views: number;
publishedAt: Date;
url: string;
numberChapter: string;
}
interface InitialReduxState {
data?: Data;
auth: Auth;
banners?: Banners;
corrections?: Corrections;
readerSettings?: ReaderSettings;
}
interface Auth {
isAuth: boolean;
user: null;
}
interface Banners {
displaySale: boolean;
}
interface Corrections {
items: null;
data: null;
}
interface Data {
bookPage: BookPage | null;
chapterPage: ChapterPage | null;
}
interface BookPage {
bookId: number;
bookmark: null;
}
interface ChapterPage {
chapterId: number;
bookId: number;
bookmark: null;
}
interface ReaderSettings {
fontSize: number;
lineHeight: number;
textIndent: number;
sidebarHide: number;
textAlign: string;
}
interface TotalData {
items: Item[];
_links: Links;
pagesData: PagesData;
}
interface Links {
self: First;
first: First;
last: First;
next: First;
}
interface First {
href: string;
}
interface Item {
id: number;
title: string;
description: string;
slug: string;
url: string;
status: string;
dislikes: number;
likes: number;
userRating: null;
lastChapter: ChapterElement;
verticalImage: Image;
horizontalImage: Image;
squareImage: Image;
}
interface PagesData {
totalCount: number;
pageCount: number;
currentPage: number;
perPage: number;
}
interface Query {
book?: string;
chapter?: string;
}
File diff suppressed because it is too large Load Diff
-440
View File
@@ -1,440 +0,0 @@
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';
import qs from 'qs';
class Zelluloza implements Plugin.PluginBase {
id = 'zelluloza';
name = 'Целлюлоза';
site = 'https://zelluloza.ru';
version = '1.0.1';
icon = 'src/ru/zelluloza/icon.png';
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const sort = showLatestNovels ? '0.0' : filters?.sort?.value || '6.0';
const genres = filters?.genres?.value || '';
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/top/freebooks/',
Origin: this.site,
},
method: 'POST',
body: qs.stringify({
op: 'morebooks',
par1: '',
par2: `125:0:${genres}:0.${sort}.0.0.0.0.0.0.0.0.0.1.s.1..:${pageNo}`,
par4: '',
}),
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[style="display: flex;"]').each((index, element) => {
novels.push({
name: loadedCheerio(element).find('span[itemprop="name"]').text() || '',
cover:
this.site +
loadedCheerio(element).find('img[class="shadow"]').attr('src'),
path: (
loadedCheerio(element).find('a[class="txt"]').attr('href') || ''
).replace(/\D/g, ''),
});
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('h2[class="bookname"]').text().trim(),
cover: this.site + loadedCheerio('img[class="shadow"]').attr('src'),
genres: loadedCheerio('.gnres').text()?.split?.(': ')?.[1],
summary:
loadedCheerio('#bann_full').text() ||
loadedCheerio('#bann_short').text(),
author: loadedCheerio('.author_link').text(),
status: loadedCheerio('.tech_decription').text().includes('Пишется')
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('ul[class="g0"] div[class="w800_m"]').each(
(chapterIndex, element) => {
const isFree = loadedCheerio(element).find(
'div[class="chaptfree"]',
).length;
if (isFree) {
const chapter = loadedCheerio(element).find('a[class="chptitle"]');
const releaseDate = loadedCheerio(element)
.find('div[class="stat"]')
.text();
chapters.push({
name: chapter.text().trim(),
path: (chapter.attr('href') || '').split('/').slice(2, 4).join('/'),
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1,
});
}
},
);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const [bookID, chapterID] = chapterPath.split('/');
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.resolveUrl(chapterPath),
Origin: this.site,
},
method: 'POST',
body: qs.stringify({
op: 'getbook',
par1: bookID,
par2: chapterID,
}),
}).then(res => res.text());
const encrypted = body.split('<END>')[0].split('\n');
const decrypted = encrypted
.map(str => decrypt(str))
.join('')
.replace(/\r/g, '')
.trim();
const chapterText = decrypted //cosmetic filters
.replace(/\[\*]([\s\S]*?)\[\/]/g, '<b>$1</b>')
.replace(/\[_]([\s\S]*?)\[\/]/g, '<u>$1</u>')
.replace(/\[-]([\s\S]*?)\[\/]/g, '<s>$1</s>')
.replace(/\[~]([\s\S]*?)\[\/]/g, '<i>$1</i>'); /*
.replace(/\[!]([\s\S]*?)\[\/]/g, '<span style="font-family: Times New Roman">$1</span>')
.replace(/\[ctr]([\s\S]*?)\[\/]/g, "<center>$1</center>")
.replace(/\[rht]([\s\S]*?)\[\/]/g, '<span style="text-align: right">$1</span>')
.replace(/\[grn]([\s\S]*?)\[\/]/g, '<span style="color: #019208">$1</span>')
.replace(/\[red]([\s\S]*?)\[\/]/g, '<span style="color: #E84040">$1</span>')
.replace(/\[blu]([\s\S]*?)\[\/]/g, '<span style="color: #354FA3">$1</span>')
.replace(/\[gry]([\s\S]*?)\[\/]/g, '<span style="color: #909090">$1</span>')
.replace(/\[geo]([\s\S]*?)\[\/]/g, '<span style="font-family: Georgia">$1</span>')
.replace(/\[gld]([\s\S]*?)\[\/]/g, '<span style="color: #988c07">$1</span>');*/
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/search/done/#result',
Origin: this.site,
},
method: 'POST',
body: qs.stringify({
op: 'morebooks',
par1: searchTerm,
par2: '206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:' + pageNo,
par4: '',
}),
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[style="display: flex;"]').each((index, element) => {
novels.push({
name: loadedCheerio(element).find('span[itemprop="name"]').text() || '',
cover:
this.site +
loadedCheerio(element).find('img[class="shadow"]').attr('src'),
path: (
loadedCheerio(element).find('a[class="txt"]').attr('href') || ''
).replace(/\D/g, ''),
});
});
return novels;
}
parseDate = (dateString: string | undefined = '') => {
const [, day, month, year, hour, minute] =
dateString.match(/(\d{2})\.(\d{2})\.(\d{4})г (\d{2}):(\d{2})/) || [];
if (day && month && year && hour && minute) {
return dayjs(
year + '-' + month + '-' + day + ' ' + hour + ':' + minute,
).format('LLL');
}
return null;
};
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + '/books/' + path;
filters = {
sort: {
label: 'Сортировка',
value: '6.0',
options: [
{ label: 'Топ-недели', value: '6.0' },
{ label: 'Бестселеры', value: '5.0' },
{ label: 'Популярные у читателей', value: '2.0' },
{ label: 'Книжные новинки', value: '0.0' },
{ label: 'Законченные книги', value: '0.1' },
],
type: FilterTypes.Picker,
},
genres: {
label: 'Жанры',
value: '',
options: [
{ label: 'Все', value: '' },
{ label: '16+', value: '204' },
{ label: '18+', value: '100' },
{ label: 'Авантюра', value: '347' },
{ label: 'Альтернативная история', value: '55' },
{ label: 'Ангст', value: '463' },
{ label: 'Аниме', value: '405' },
{ label: 'Антиутопия', value: '21' },
{ label: 'Апокалипсис', value: '368' },
{ label: 'Аудиокнига', value: '391' },
{ label: 'Байки', value: '435' },
{ label: 'Бизнес-книги', value: '522' },
{ label: 'Биография', value: '52' },
{ label: 'Биопанк', value: '379' },
{ label: 'Боевая фантастика', value: '512' },
{ label: 'Боевик', value: '61' },
{ label: 'Боевые искусства', value: '142' },
{ label: 'Бояръ-аниме', value: '433' },
{ label: 'Вбоквелл', value: '372' },
{ label: 'Вестерн', value: '96' },
{ label: 'Война миров', value: '137' },
{ label: 'Восточное фэнтези', value: '505' },
{ label: 'Городское фентези', value: '63' },
{ label: 'Городское фэнтези', value: '520' },
{ label: 'Готика', value: '83' },
{ label: 'Детективы', value: '4' },
{ label: 'Детские книги', value: '8' },
{ label: 'Документальные книги', value: '351' },
{ label: 'Дом и дача', value: '534' },
{ label: 'Дорожные истории', value: '41' },
{ label: 'Драма', value: '47' },
{ label: 'Древняя литература', value: '529' },
{ label: 'Женский психологический роман', value: '186' },
{ label: 'Женский роман', value: '141' },
{ label: 'Зарубежная классика', value: '524' },
{ label: 'Здоровье и красота', value: '533' },
{ label: 'Изучение языков', value: '537' },
{ label: 'Иронический детектив', value: '85' },
{ label: 'Исследования', value: '156' },
{ label: 'Историческая литература', value: '3' },
{ label: 'Исторический любовный роман', value: '518' },
{ label: 'Историческое фэнтези', value: '90' },
{ label: 'Киберпанк', value: '7' },
{ label: 'Классическая литература', value: '9' },
{ label: 'Книга-игра', value: '371' },
{ label: 'Книги для родителей', value: '535' },
{ label: 'Комедия', value: '80' },
{ label: 'Конкурсные сборники', value: '349' },
{ label: 'Короткий любовный роман', value: '517' },
{ label: 'Космическая фантастика', value: '513' },
{ label: 'Космоопера', value: '58' },
{ label: 'Криминальная проза', value: '35' },
{ label: 'Криптоистория', value: '400' },
{ label: 'Ксенофантастика', value: '102' },
{ label: 'Культура и искусство', value: '536' },
{ label: 'Легенды', value: '161' },
{ label: 'Лирика', value: '49' },
{ label: 'ЛитРПГ', value: '62' },
{ label: 'Любовная фантастика', value: '516' },
{ label: 'Любовное фэнтези', value: '515' },
{ label: 'Любовный роман', value: '46' },
{ label: 'Магический реализм', value: '42' },
{ label: 'Матриархат', value: '385' },
{ label: 'Медицина', value: '57' },
{ label: 'Мелодрама', value: '45' },
{ label: 'Мемуары', value: '532' },
{ label: 'Метафизика', value: '430' },
{ label: 'Милитари', value: '126' },
{ label: 'Мистика', value: '22' },
{ label: 'Морские приключения', value: '211' },
{ label: 'Мотивация', value: '135' },
{ label: 'Научная фантастика', value: '122' },
{ label: 'Научно-популярная', value: '43' },
{ label: 'Ненаучная фантастика', value: '387' },
{ label: 'Новелла', value: '498' },
{ label: 'Нон-фикшн', value: '507' },
{ label: 'Нуар', value: '121' },
{ label: 'О войне', value: '136' },
{ label: 'Оккультизм', value: '424' },
{ label: 'Параллельные миры', value: '438' },
{ label: 'Пародия', value: '108' },
{ label: 'Подростковая фантастика', value: '27' },
{ label: 'Подростковый роман', value: '388' },
{ label: 'Подростковый ужастик', value: '110' },
{ label: 'Постапокалипсис', value: '82' },
{ label: 'Постапокалиптика', value: '67' },
{ label: 'Поэзия', value: '44' },
{ label: 'Превращение', value: '170' },
{ label: 'Приключение', value: '497' },
{ label: 'Приключения', value: '5' },
{ label: 'Природа и животные', value: '538' },
{ label: 'Проза', value: '95' },
{ label: 'Производственный роман', value: '148' },
{ label: 'Психология', value: '31' },
{ label: 'Путешествия', value: '383' },
{ label: 'Рассказы', value: '101' },
{ label: 'Реализм', value: '422' },
{ label: 'РеалРПГ', value: '442' },
{ label: 'Религия', value: '188' },
{ label: 'Робинзонада', value: '434' },
{ label: 'Романтика', value: '130' },
{ label: 'Романтическая фантастика', value: '501' },
{ label: 'Русская классика', value: '523' },
{ label: 'Рыцарский роман', value: '112' },
{ label: 'Символизм', value: '439' },
{ label: 'Сказки', value: '93' },
{ label: 'Славянское фэнтези', value: '508' },
{ label: 'Сновидения', value: '428' },
{ label: 'Современный любовный роман', value: '519' },
{ label: 'Социально-психологическая фантастика', value: '98' },
{ label: 'Спорт', value: '147' },
{ label: 'Справочники', value: '528' },
{ label: 'Стимпанк', value: '56' },
{ label: 'Субкультура', value: '209' },
{ label: 'Сюрреализм', value: '26' },
{ label: 'Тёмное фэнтези', value: '437' },
{ label: 'Техномагия', value: '159' },
{ label: 'Триллер', value: '6' },
{ label: 'Трэш', value: '162' },
{ label: 'Ужасы', value: '531' },
{ label: 'Утопия', value: '191' },
{ label: 'Учебник', value: '411' },
{ label: 'Фантастика', value: '1' },
{ label: 'Фантастический боевик', value: '38' },
{ label: 'Фанфик', value: '111' },
{ label: 'Философия', value: '461' },
{ label: 'Фурри', value: '394' },
{ label: 'Фэнтези', value: '2' },
{ label: 'Хентай', value: '441' },
{ label: 'Хобби и досуг', value: '530' },
{ label: 'Хоррор', value: '86' },
{ label: 'Чёрный юмор', value: '73' },
{ label: 'Эзотерика', value: '30' },
{ label: 'Экшн', value: '94' },
{ label: 'Эпическое фэнтези', value: '499' },
{ label: 'Эротика', value: '19' },
{ label: 'Эссе', value: '353' },
{ label: 'ЭТТИ', value: '502' },
{ label: 'Юмористическая литература', value: '20' },
{ label: 'Юмористическая фантастика', value: '514' },
{ label: 'Юмористическое фэнтези', value: '521' },
{ label: 'EVE - Миры Содружества', value: '99' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
const alphabet: { [key: string]: string } = {
'~': '0',
'H': '1',
'^': '2',
'@': '3',
'f': '4',
'0': '5',
'5': '6',
'n': '7',
'r': '8',
'=': '9',
'W': 'a',
'L': 'b',
'7': 'c',
' ': 'd',
'u': 'e',
'c': 'f',
};
function decrypt(encrypt: string) {
if (!encrypt) return '';
const hexArray = [];
for (let j = 0; j < encrypt.length; j += 2) {
const firstChar = encrypt.substring(j, j + 1);
const secondChar = encrypt.substring(j + 1, j + 2);
hexArray.push(alphabet[firstChar] + alphabet[secondChar]);
}
return '<p>' + hexToUtf8(hexArray) + '</p>';
}
function hexToUtf8(hexArray: string[]) {
let index = 0;
let result = '';
while (index < hexArray.length) {
const currentHex = parseInt(hexArray[index], 16) & 0xff;
if (currentHex < 128) {
if (currentHex < 16) {
switch (currentHex) {
case 9:
result += ' ';
break;
case 13:
result += '\r';
break;
case 10:
result += '\n';
break;
}
} else {
result += String.fromCharCode(currentHex);
}
index++;
} else if (currentHex > 191 && currentHex < 224) {
if (index + 1 < hexArray.length) {
const nextHex = parseInt(hexArray[index + 1], 16) & 0xff;
result += String.fromCharCode(
((currentHex & 31) << 6) | (nextHex & 63),
);
}
index += 2;
} else {
if (index + 2 < hexArray.length) {
const nextHex = parseInt(hexArray[index + 1], 16) & 0xff;
const thirdHex = parseInt(hexArray[index + 2], 16) & 0xff;
result += String.fromCharCode(
((currentHex & 15) << 12) | ((nextHex & 63) << 6) | (thirdHex & 63),
);
}
index += 3;
}
}
return result;
}
export default new Zelluloza();