diff --git a/icons/src/ru/zelluloza/icon.png b/icons/src/ru/zelluloza/icon.png new file mode 100644 index 0000000..a1c5bab Binary files /dev/null and b/icons/src/ru/zelluloza/icon.png differ diff --git a/plugins/russian/zelluloza.ts b/plugins/russian/zelluloza.ts new file mode 100644 index 0000000..c8afc62 --- /dev/null +++ b/plugins/russian/zelluloza.ts @@ -0,0 +1,440 @@ +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, + ): Promise { + 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 { + 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 { + 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("")[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: 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 "

" + hexToUtf8(hexArray) + "

"; +} + +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();