Feat: Vite + React (#991)

* remove test_web

* integrate reactjs-vite

* update lint-staged

* simple cors proxy server which handles cookies and redirections

* overrider global fetch with proxy call

* simple examples for set cookies and fetch request

* app bar

* add redux

* loadPlugins.
error: cant load urlencode module

* add ts-node for transforming

* plugin search & select

* plugin slice

* accordion components

* path item & popular novel

* optional for filters

* search novel

* parse novel

* parseChapter

* update cookies

* default filters

* fix: import path

* bundler module resolution

* new json_plugins

* fix: generate multisrc (missing filters)

* fix: host scripts

* fix: plugin code path

* remove typechecking action
This commit is contained in:
nyagami
2024-06-16 18:13:41 +07:00
committed by GitHub
parent a4cb781f32
commit d36e8ec85f
333 changed files with 5857 additions and 28579 deletions
-440
View File
@@ -1,440 +0,0 @@
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
import qs from 'qs';
class Zelluloza implements Plugin.PluginBase {
id = 'zelluloza';
name = 'Целлюлоза';
site = 'https://zelluloza.ru';
version = '1.0.1';
icon = 'src/ru/zelluloza/icon.png';
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const sort = showLatestNovels ? '0.0' : filters?.sort?.value || '6.0';
const genres = filters?.genres?.value || '';
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/top/freebooks/',
Origin: this.site,
},
method: 'POST',
body: qs.stringify({
op: 'morebooks',
par1: '',
par2: `125:0:${genres}:0.${sort}.0.0.0.0.0.0.0.0.0.1.s.1..:${pageNo}`,
par4: '',
}),
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[style="display: flex;"]').each((index, element) => {
novels.push({
name: loadedCheerio(element).find('span[itemprop="name"]').text() || '',
cover:
this.site +
loadedCheerio(element).find('img[class="shadow"]').attr('src'),
path: (
loadedCheerio(element).find('a[class="txt"]').attr('href') || ''
).replace(/\D/g, ''),
});
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('h2[class="bookname"]').text().trim(),
cover: this.site + loadedCheerio('img[class="shadow"]').attr('src'),
genres: loadedCheerio('.gnres').text()?.split?.(': ')?.[1],
summary:
loadedCheerio('#bann_full').text() ||
loadedCheerio('#bann_short').text(),
author: loadedCheerio('.author_link').text(),
status: loadedCheerio('.tech_decription').text().includes('Пишется')
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('ul[class="g0"] div[class="w800_m"]').each(
(chapterIndex, element) => {
const isFree = loadedCheerio(element).find(
'div[class="chaptfree"]',
).length;
if (isFree) {
const chapter = loadedCheerio(element).find('a[class="chptitle"]');
const releaseDate = loadedCheerio(element)
.find('div[class="stat"]')
.text();
chapters.push({
name: chapter.text().trim(),
path: (chapter.attr('href') || '').split('/').slice(2, 4).join('/'),
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1,
});
}
},
);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const [bookID, chapterID] = chapterPath.split('/');
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.resolveUrl(chapterPath),
Origin: this.site,
},
method: 'POST',
body: qs.stringify({
op: 'getbook',
par1: bookID,
par2: chapterID,
}),
}).then(res => res.text());
const encrypted = body.split('<END>')[0].split('\n');
const decrypted = encrypted
.map(str => decrypt(str))
.join('')
.replace(/\r/g, '')
.trim();
const chapterText = decrypted //cosmetic filters
.replace(/\[\*]([\s\S]*?)\[\/]/g, '<b>$1</b>')
.replace(/\[_]([\s\S]*?)\[\/]/g, '<u>$1</u>')
.replace(/\[-]([\s\S]*?)\[\/]/g, '<s>$1</s>')
.replace(/\[~]([\s\S]*?)\[\/]/g, '<i>$1</i>'); /*
.replace(/\[!]([\s\S]*?)\[\/]/g, '<span style="font-family: Times New Roman">$1</span>')
.replace(/\[ctr]([\s\S]*?)\[\/]/g, "<center>$1</center>")
.replace(/\[rht]([\s\S]*?)\[\/]/g, '<span style="text-align: right">$1</span>')
.replace(/\[grn]([\s\S]*?)\[\/]/g, '<span style="color: #019208">$1</span>')
.replace(/\[red]([\s\S]*?)\[\/]/g, '<span style="color: #E84040">$1</span>')
.replace(/\[blu]([\s\S]*?)\[\/]/g, '<span style="color: #354FA3">$1</span>')
.replace(/\[gry]([\s\S]*?)\[\/]/g, '<span style="color: #909090">$1</span>')
.replace(/\[geo]([\s\S]*?)\[\/]/g, '<span style="font-family: Georgia">$1</span>')
.replace(/\[gld]([\s\S]*?)\[\/]/g, '<span style="color: #988c07">$1</span>');*/
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/search/done/#result',
Origin: this.site,
},
method: 'POST',
body: qs.stringify({
op: 'morebooks',
par1: searchTerm,
par2: '206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:' + pageNo,
par4: '',
}),
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[style="display: flex;"]').each((index, element) => {
novels.push({
name: loadedCheerio(element).find('span[itemprop="name"]').text() || '',
cover:
this.site +
loadedCheerio(element).find('img[class="shadow"]').attr('src'),
path: (
loadedCheerio(element).find('a[class="txt"]').attr('href') || ''
).replace(/\D/g, ''),
});
});
return novels;
}
parseDate = (dateString: string | undefined = '') => {
const [, day, month, year, hour, minute] =
dateString.match(/(\d{2})\.(\d{2})\.(\d{4})г (\d{2}):(\d{2})/) || [];
if (day && month && year && hour && minute) {
return dayjs(
year + '-' + month + '-' + day + ' ' + hour + ':' + minute,
).format('LLL');
}
return null;
};
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + '/books/' + path;
filters = {
sort: {
label: 'Сортировка',
value: '6.0',
options: [
{ label: 'Топ-недели', value: '6.0' },
{ label: 'Бестселеры', value: '5.0' },
{ label: 'Популярные у читателей', value: '2.0' },
{ label: 'Книжные новинки', value: '0.0' },
{ label: 'Законченные книги', value: '0.1' },
],
type: FilterTypes.Picker,
},
genres: {
label: 'Жанры',
value: '',
options: [
{ label: 'Все', value: '' },
{ label: '16+', value: '204' },
{ label: '18+', value: '100' },
{ label: 'Авантюра', value: '347' },
{ label: 'Альтернативная история', value: '55' },
{ label: 'Ангст', value: '463' },
{ label: 'Аниме', value: '405' },
{ label: 'Антиутопия', value: '21' },
{ label: 'Апокалипсис', value: '368' },
{ label: 'Аудиокнига', value: '391' },
{ label: 'Байки', value: '435' },
{ label: 'Бизнес-книги', value: '522' },
{ label: 'Биография', value: '52' },
{ label: 'Биопанк', value: '379' },
{ label: 'Боевая фантастика', value: '512' },
{ label: 'Боевик', value: '61' },
{ label: 'Боевые искусства', value: '142' },
{ label: 'Бояръ-аниме', value: '433' },
{ label: 'Вбоквелл', value: '372' },
{ label: 'Вестерн', value: '96' },
{ label: 'Война миров', value: '137' },
{ label: 'Восточное фэнтези', value: '505' },
{ label: 'Городское фентези', value: '63' },
{ label: 'Городское фэнтези', value: '520' },
{ label: 'Готика', value: '83' },
{ label: 'Детективы', value: '4' },
{ label: 'Детские книги', value: '8' },
{ label: 'Документальные книги', value: '351' },
{ label: 'Дом и дача', value: '534' },
{ label: 'Дорожные истории', value: '41' },
{ label: 'Драма', value: '47' },
{ label: 'Древняя литература', value: '529' },
{ label: 'Женский психологический роман', value: '186' },
{ label: 'Женский роман', value: '141' },
{ label: 'Зарубежная классика', value: '524' },
{ label: 'Здоровье и красота', value: '533' },
{ label: 'Изучение языков', value: '537' },
{ label: 'Иронический детектив', value: '85' },
{ label: 'Исследования', value: '156' },
{ label: 'Историческая литература', value: '3' },
{ label: 'Исторический любовный роман', value: '518' },
{ label: 'Историческое фэнтези', value: '90' },
{ label: 'Киберпанк', value: '7' },
{ label: 'Классическая литература', value: '9' },
{ label: 'Книга-игра', value: '371' },
{ label: 'Книги для родителей', value: '535' },
{ label: 'Комедия', value: '80' },
{ label: 'Конкурсные сборники', value: '349' },
{ label: 'Короткий любовный роман', value: '517' },
{ label: 'Космическая фантастика', value: '513' },
{ label: 'Космоопера', value: '58' },
{ label: 'Криминальная проза', value: '35' },
{ label: 'Криптоистория', value: '400' },
{ label: 'Ксенофантастика', value: '102' },
{ label: 'Культура и искусство', value: '536' },
{ label: 'Легенды', value: '161' },
{ label: 'Лирика', value: '49' },
{ label: 'ЛитРПГ', value: '62' },
{ label: 'Любовная фантастика', value: '516' },
{ label: 'Любовное фэнтези', value: '515' },
{ label: 'Любовный роман', value: '46' },
{ label: 'Магический реализм', value: '42' },
{ label: 'Матриархат', value: '385' },
{ label: 'Медицина', value: '57' },
{ label: 'Мелодрама', value: '45' },
{ label: 'Мемуары', value: '532' },
{ label: 'Метафизика', value: '430' },
{ label: 'Милитари', value: '126' },
{ label: 'Мистика', value: '22' },
{ label: 'Морские приключения', value: '211' },
{ label: 'Мотивация', value: '135' },
{ label: 'Научная фантастика', value: '122' },
{ label: 'Научно-популярная', value: '43' },
{ label: 'Ненаучная фантастика', value: '387' },
{ label: 'Новелла', value: '498' },
{ label: 'Нон-фикшн', value: '507' },
{ label: 'Нуар', value: '121' },
{ label: 'О войне', value: '136' },
{ label: 'Оккультизм', value: '424' },
{ label: 'Параллельные миры', value: '438' },
{ label: 'Пародия', value: '108' },
{ label: 'Подростковая фантастика', value: '27' },
{ label: 'Подростковый роман', value: '388' },
{ label: 'Подростковый ужастик', value: '110' },
{ label: 'Постапокалипсис', value: '82' },
{ label: 'Постапокалиптика', value: '67' },
{ label: 'Поэзия', value: '44' },
{ label: 'Превращение', value: '170' },
{ label: 'Приключение', value: '497' },
{ label: 'Приключения', value: '5' },
{ label: 'Природа и животные', value: '538' },
{ label: 'Проза', value: '95' },
{ label: 'Производственный роман', value: '148' },
{ label: 'Психология', value: '31' },
{ label: 'Путешествия', value: '383' },
{ label: 'Рассказы', value: '101' },
{ label: 'Реализм', value: '422' },
{ label: 'РеалРПГ', value: '442' },
{ label: 'Религия', value: '188' },
{ label: 'Робинзонада', value: '434' },
{ label: 'Романтика', value: '130' },
{ label: 'Романтическая фантастика', value: '501' },
{ label: 'Русская классика', value: '523' },
{ label: 'Рыцарский роман', value: '112' },
{ label: 'Символизм', value: '439' },
{ label: 'Сказки', value: '93' },
{ label: 'Славянское фэнтези', value: '508' },
{ label: 'Сновидения', value: '428' },
{ label: 'Современный любовный роман', value: '519' },
{ label: 'Социально-психологическая фантастика', value: '98' },
{ label: 'Спорт', value: '147' },
{ label: 'Справочники', value: '528' },
{ label: 'Стимпанк', value: '56' },
{ label: 'Субкультура', value: '209' },
{ label: 'Сюрреализм', value: '26' },
{ label: 'Тёмное фэнтези', value: '437' },
{ label: 'Техномагия', value: '159' },
{ label: 'Триллер', value: '6' },
{ label: 'Трэш', value: '162' },
{ label: 'Ужасы', value: '531' },
{ label: 'Утопия', value: '191' },
{ label: 'Учебник', value: '411' },
{ label: 'Фантастика', value: '1' },
{ label: 'Фантастический боевик', value: '38' },
{ label: 'Фанфик', value: '111' },
{ label: 'Философия', value: '461' },
{ label: 'Фурри', value: '394' },
{ label: 'Фэнтези', value: '2' },
{ label: 'Хентай', value: '441' },
{ label: 'Хобби и досуг', value: '530' },
{ label: 'Хоррор', value: '86' },
{ label: 'Чёрный юмор', value: '73' },
{ label: 'Эзотерика', value: '30' },
{ label: 'Экшн', value: '94' },
{ label: 'Эпическое фэнтези', value: '499' },
{ label: 'Эротика', value: '19' },
{ label: 'Эссе', value: '353' },
{ label: 'ЭТТИ', value: '502' },
{ label: 'Юмористическая литература', value: '20' },
{ label: 'Юмористическая фантастика', value: '514' },
{ label: 'Юмористическое фэнтези', value: '521' },
{ label: 'EVE - Миры Содружества', value: '99' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
const alphabet: { [key: string]: string } = {
'~': '0',
'H': '1',
'^': '2',
'@': '3',
'f': '4',
'0': '5',
'5': '6',
'n': '7',
'r': '8',
'=': '9',
'W': 'a',
'L': 'b',
'7': 'c',
' ': 'd',
'u': 'e',
'c': 'f',
};
function decrypt(encrypt: string) {
if (!encrypt) return '';
const hexArray = [];
for (let j = 0; j < encrypt.length; j += 2) {
const firstChar = encrypt.substring(j, j + 1);
const secondChar = encrypt.substring(j + 1, j + 2);
hexArray.push(alphabet[firstChar] + alphabet[secondChar]);
}
return '<p>' + hexToUtf8(hexArray) + '</p>';
}
function hexToUtf8(hexArray: string[]) {
let index = 0;
let result = '';
while (index < hexArray.length) {
const currentHex = parseInt(hexArray[index], 16) & 0xff;
if (currentHex < 128) {
if (currentHex < 16) {
switch (currentHex) {
case 9:
result += ' ';
break;
case 13:
result += '\r';
break;
case 10:
result += '\n';
break;
}
} else {
result += String.fromCharCode(currentHex);
}
index++;
} else if (currentHex > 191 && currentHex < 224) {
if (index + 1 < hexArray.length) {
const nextHex = parseInt(hexArray[index + 1], 16) & 0xff;
result += String.fromCharCode(
((currentHex & 31) << 6) | (nextHex & 63),
);
}
index += 2;
} else {
if (index + 2 < hexArray.length) {
const nextHex = parseInt(hexArray[index + 1], 16) & 0xff;
const thirdHex = parseInt(hexArray[index + 2], 16) & 0xff;
result += String.fromCharCode(
((currentHex & 15) << 12) | ((nextHex & 63) << 6) | (thirdHex & 63),
);
}
index += 3;
}
}
return result;
}
export default new Zelluloza();