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"; class Bookriver implements Plugin.PluginBase { id = "bookriver"; name = "Bookriver"; site = "https://bookriver.ru"; version = "1.0.0"; icon = "src/ru/bookriver/icon.png"; async popularNovels( pageNo: number, { showLatestNovels, filters, }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + "/genre?page=" + pageNo + "&perPage=24&sortingType="; url += showLatestNovels ? "last-update" : filters?.sort?.value || "bestseller"; if (filters?.genres?.value?.length) { url += "&g=" + filters.genres.value.join(","); } const result = await fetchApi(url).then((res) => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = []; const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); if (jsonRaw) { const json: response = JSON.parse(jsonRaw); json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) => novels.push({ name: novel.name, cover: novel.coverImages[0].url, path: novel.slug, }), ); } return novels; } async parseNovel(novelPath: string): Promise { const result = await fetchApi(this.resolveUrl(novelPath, true)) .then((res) => res.text()); const loadedCheerio = parseHTML(result); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); const json: response = JSON.parse(jsonRaw || "{}"); const book = json.props.pageProps.state.book?.bookPage; const novel: Plugin.SourceNovel = { path: novelPath, name: book?.name || "", cover: book?.coverImages[0].url, summary: book?.annotation, author: book?.author?.name, genres: book?.tags?.map((tag) => tag.name).join(", "), status: book?.statusComplete === "writing" ? NovelStatus.Ongoing : NovelStatus.Completed, }; const chapters: Plugin.ChapterItem[] = []; book?.ebook?.chapters?.forEach((chapter, chapterIndex) => { if (chapter.available) { chapters.push({ name: chapter.name, path: book?.slug + "/" + chapter.chapterId, releaseTime: dayjs( chapter.firstPublishedAt || chapter.createdAt || undefined, ).format("LLL"), chapterNumber: chapterIndex + 1, }); } }); novel.chapters = chapters; return novel; } async parseChapter(chapterPath: string): Promise { const url = "https://api.bookriver.ru/api/v1/books/chapter/text/"; const result = await fetchApi(url + chapterPath.split("/").pop()); const json = (await result.json()) as responseChapter; let chapterText = json.data.content || "Конец произведения"; if (json.data?.audio?.available) { chapterText += "\n" + json.data.audio.url; } return chapterText; } async searchNovels( searchTerm: string, pageNo: number | undefined = 1, ): Promise { const url = "https://api.bookriver.ru/api/v1/search/autocomplete?keyword=" + searchTerm + "&page=" + pageNo + "&perPage=10"; const result = await fetchApi(url); const json = (await result.json()) as responseSearch; const novels: Plugin.NovelItem[] = []; json.data?.books?.forEach((novel) => novels.push({ name: novel.name, cover: novel.coverImages[0].url, path: novel.slug, }), ); return novels; } fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => this.site + (isNovel ? "/book/" : "/reader/") + path; filters = { sort: { label: "Сортировка", value: "bestseller", options: [ { label: "Бестселлеры", value: "bestseller" }, { label: "Дате добавления", value: "newest" }, { label: "Дате обновления", value: "last-update" }, ], type: FilterTypes.Picker, }, genres: { label: "жанры", value: [], options: [ { label: "Альтернативная история", value: "alternativnaya-istoriya" }, { label: "Боевая фантастика", value: "boevaya-fantastika" }, { label: "Боевое фэнтези", value: "boevoe-fentezi" }, { label: "Бытовое фэнтези", value: "bytovoe-fentezi" }, { label: "Героическая фантастика", value: "geroicheskaya-fantastika" }, { label: "Героическое фэнтези", value: "geroicheskoe-fentezi" }, { label: "Городское фэнтези", value: "gorodskoe-fentezi" }, { label: "Детектив", value: "detektiv" }, { label: "Детективная фантастика", value: "detektivnaya-fantastika" }, { label: "Жёсткая эротика", value: "zhyostkaya-erotika" }, { label: "Исторический детектив", value: "istoricheskii-detektiv" }, { label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" }, { label: "Историческое фэнтези", value: "istoricheskoe-fentezi" }, { label: "Киберпанк", value: "kiberpank" }, { label: "Классический детектив", value: "klassicheskii-detektiv" }, { label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" }, { label: "Космическая фантастика", value: "kosmicheskaya-fantastika" }, { label: "Криминальный детектив", value: "kriminalnyi-detektiv" }, { label: "ЛитРПГ", value: "litrpg" }, { label: "Любовная фантастика", value: "lyubovnaya-fantastika" }, { label: "Любовное фэнтези", value: "lyubovnoe-fentezi" }, { label: "Любовный роман", value: "lyubovnyi-roman" }, { label: "Мистика", value: "mistika" }, { label: "Молодежная проза", value: "molodezhnaya-proza" }, { label: "Научная фантастика", value: "nauchnaya-fantastika" }, { label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" }, { label: "Политический детектив", value: "politicheskii-detektiv" }, { label: "Попаданцы", value: "popadantsy" }, { label: "Постапокалипсис", value: "postapokalipsis" }, { label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" }, { label: "Романтическая эротика", value: "romanticheskaya-erotika" }, { label: "С элементами эротики", value: "s-elementami-erotiki" }, { label: "Славянское фэнтези", value: "slavyanskoe-fentezi" }, { label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" }, { label: "Социальная фантастика", value: "sotsialnaya-fantastika" }, { label: "Тёмное фэнтези", value: "temnoe-fentezi" }, { label: "Фантастика", value: "fantastika" }, { label: "Фэнтези", value: "fentezi" }, { label: "Шпионский детектив", value: "shpionskii-detektiv" }, { label: "Эпическое фэнтези", value: "epicheskoe-fentezi" }, { label: "Эротика", value: "erotika" }, { label: "Эротическая фантастика", value: "eroticheskaya-fantastika" }, { label: "Эротический фанфик", value: "eroticheskii-fanfik" }, { label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" }, { label: "Юмористический детектив", value: "yumoristicheskii-detektiv" }, { label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" }, ], type: FilterTypes.CheckboxGroup, }, } satisfies Filters; } export default new Bookriver(); interface response { props: { pageProps: { state: { book?: { bookPage: BooksEntity; }; pagesFilter?: { genre: { books: BooksEntity[]; }; }; }; }; }; } interface BooksEntity { name: string; coverImages: { url: string }[]; slug: string; annotation?: string; author?: { name: string; }; tags?: { name: string }[]; ebook?: { chapters: Chapter[]; }; statusComplete: string; } interface Chapter { name: string; available: boolean; firstPublishedAt?: Date | null; createdAt?: Date | null; chapterId: number | string; } interface responseChapter { data: { id: number; bookId: number; name: string; content: string; symbols: number; number: number; authorPages: number; createdAt: string; updatedAt: string; firstPublishedAt: string; status: string; protected: boolean; free: boolean; audio?: { available: boolean; url: string; duration: number; }; publicationScheduledFor?: null; }; } interface responseSearch { data: Data; total: number; } interface Data { books?: BooksEntity[] | null; }