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:
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
Reference in New Issue
Block a user