5f9a067ab9
* add prettier * lint-staged * prettier * upgrade husky
444 lines
17 KiB
TypeScript
444 lines
17 KiB
TypeScript
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.0';
|
||
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('img[class="shadow"]').attr('onclick') ||
|
||
''
|
||
).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('span[itemprop="name"]').text(),
|
||
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('img[class="shadow"]').attr('onclick') ||
|
||
''
|
||
).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;
|
||
};
|
||
|
||
fetchImage = fetchFile;
|
||
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();
|