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, ): Promise { 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 { 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 { 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('')[0].split('\n'); const decrypted = encrypted .map(str => decrypt(str)) .join('') .replace(/\r/g, '') .trim(); const chapterText = decrypted //cosmetic filters .replace(/\[\*]([\s\S]*?)\[\/]/g, '$1') .replace(/\[_]([\s\S]*?)\[\/]/g, '$1') .replace(/\[-]([\s\S]*?)\[\/]/g, '$1') .replace(/\[~]([\s\S]*?)\[\/]/g, '$1'); /* .replace(/\[!]([\s\S]*?)\[\/]/g, '$1') .replace(/\[ctr]([\s\S]*?)\[\/]/g, "
$1
") .replace(/\[rht]([\s\S]*?)\[\/]/g, '$1') .replace(/\[grn]([\s\S]*?)\[\/]/g, '$1') .replace(/\[red]([\s\S]*?)\[\/]/g, '$1') .replace(/\[blu]([\s\S]*?)\[\/]/g, '$1') .replace(/\[gry]([\s\S]*?)\[\/]/g, '$1') .replace(/\[geo]([\s\S]*?)\[\/]/g, '$1') .replace(/\[gld]([\s\S]*?)\[\/]/g, '$1');*/ return chapterText; } async searchNovels( searchTerm: string, pageNo: number, ): Promise { 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 = { '~': '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 '

' + decodeURIComponent('%' + hexArray.join('%')) + '

'; } export default new Zelluloza();