Files
lnreader-plugins/plugins/russian/zelluloza.ts
T
Rajarshee Chatterjee 20c2b75b01 refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc

* feat: Overhaul Playground UI

* feat: Add Filters to New UI

* feat: Persist Selected Plugin

* feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts

[skip ci]

* feat: UX Improvements

* chore: Update Plugin Path in Publish Workflow

* fix: Add Backward Compatibility

* chore: Update Plugin Publishing Script
2025-10-20 22:54:08 +05:30

397 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plugin } from '@/types/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
class Zelluloza implements Plugin.PluginBase {
id = 'zelluloza';
name = 'Целлюлоза';
site = 'https://zelluloza.ru';
version = '1.0.3';
icon = 'src/ru/zelluloza/icon.png';
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const sort = showLatestNovels ? '0' : filters?.sort?.value || '3';
const genres = filters?.genres?.value || '0';
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: new URLSearchParams({
op: 'morebooks',
par1: '',
par2: `206:0:${genres}:0.${sort}.0.0.0.0.0.0.0.0.0.0.0..0..:${pageNo}`,
par4: '',
}).toString(),
}).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('a[class="txt"]').attr('title') || '',
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)).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 span[itemprop="genre"]')
.map((index, element) => loadedCheerio(element).text())
.get()
.join(','),
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: new URLSearchParams({
op: 'getbook',
par1: bookID,
par2: chapterID,
}).toString(),
}).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: new URLSearchParams({
op: 'morebooks',
par1: searchTerm || '',
par2: '206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:' + pageNo,
par4: '',
}).toString(),
}).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('a[class="txt"]').attr('title') || '',
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) => this.site + '/books/' + path;
filters = {
sort: {
label: 'Сортировка',
value: '3',
options: [
{ label: 'По рейтингу', value: '3' },
{ label: 'По изменению', value: '0' },
{ label: 'По длительности чтения', value: '1' },
{ label: 'По количеству читателей', value: '2' },
{ label: 'По популярности', value: '4' },
{ label: 'Самые продаваемые', value: '5' },
],
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: Record<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>' + decodeURIComponent('%' + hexArray.join('%')) + '</p>';
}
export default new Zelluloza();