import { Chapter, Novel, Plugin } from "@typings/plugin"; import { FilterInputs } from "@libs/filterInputs"; import { defaultCover } from "@libs/defaultCover"; import { fetchApi, fetchFile } from "@libs/fetch"; import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; import dayjs from "dayjs"; export const id = "RNRF"; export const name = "РанобэРФ"; export const site = "https://ранобэ.рф"; export const version = "1.0.0"; export const icon = "src/ru/ranoberf/icon.png"; export const popularNovels: Plugin.popularNovels = async function ( page, { showLatestNovels, filters } ) { let url = site + "/books?order="; url += showLatestNovels ? "lastPublishedChapter" : filters?.sort || "popular"; url += "&page=" + page; const result = await fetchApi(url); const body = await result.text(); const loadedCheerio = parseHTML(body); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); let json: response = JSON.parse(jsonRaw || '{}'); let novels: Novel.Item[] = []; json.props.pageProps?.totalData?.items?.forEach((novel) => novels.push({ name: novel.title, cover: novel?.verticalImage?.url ? site + novel.verticalImage.url : defaultCover, url: site + "/" + novel.slug, }) ); return novels; }; export const parseNovelAndChapters: Plugin.parseNovelAndChapters = async function (novelUrl) { const result = await fetchApi(novelUrl); const body = await result.text(); const loadedCheerio = parseHTML(body); const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); const book = (JSON.parse(jsonRaw || '{}') as response).props.pageProps.book; let novel: Novel.instance = { url: novelUrl, name: book?.title, cover: book?.verticalImage?.url ? site + book.verticalImage.url : defaultCover, summary: book?.description, author: book?.author || "", genres: book?.genres.map((item) => item.title).join(", "), status: book?.additionalInfo.includes("Активен") ? NovelStatus.Ongoing : NovelStatus.Completed, }; let chapters: Chapter.Item[] = []; book?.chapters?.forEach((chapter) => { if (!chapter?.isDonate || chapter?.isUserPaid) { chapters.push({ name: chapter.title, releaseTime: dayjs(chapter.publishedAt).format("LLL"), url: site + chapter.url, }); } }); novel.chapters = chapters.reverse(); return novel; }; export const parseChapter: Plugin.parseChapter = async function (chapterUrl) { const result = await fetchApi(chapterUrl); const body = await result.text(); let loadedCheerio = parseHTML(body); let jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); let json: response = JSON.parse(jsonRaw || '{}'); loadedCheerio = parseHTML(json.props.pageProps?.chapter?.content?.text || ""); loadedCheerio("img").each(function () { if (!loadedCheerio(this).attr("src")?.startsWith("http")) { const src = loadedCheerio(this).attr("src"); loadedCheerio(this).attr("src", site + src); } }); const chapterText = loadedCheerio.html(); return chapterText; }; export const searchNovels: Plugin.searchNovels = async function (searchTerm) { const url = `${site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`; const result = await fetchApi(url); const body = (await result.json()) as { items: Item[] }; let novels: Novel.Item[] = []; body.items.forEach((novel) => novels.push({ name: novel.title, cover: novel?.verticalImage?.url ? site + novel.verticalImage.url : defaultCover, url: site + "/" + novel.slug, }) ); return novels; }; export const fetchImage: Plugin.fetchImage = fetchFile; export const filters = [ { key: "sort", label: "Сортировка", values: [ { label: "Рейтинг", value: "popular" }, { label: "Дате добавления", value: "new" }, { label: "Дате обновления", value: "lastPublishedChapter" }, { label: "Законченные", value: "completed" }, ], inputType: FilterInputs.Picker, }, ]; interface response { props: Props; page: string; query: Query; buildId: string; isFallback: boolean; gssp: boolean; appGip: boolean; dynamicIds?: string[]; } interface Props { pageProps: PageProps; isMobile: boolean; theme: string; __N_SSP: boolean; } interface PageProps { initialReduxState: InitialReduxState; book?: PagePropsBook; totalData?: TotalData; layoutViewCookies?: string; displayBanner?: boolean; chapter?: PagePropsChapter; } interface PagePropsBook { id: number; countryId: number; creatorId: number; lastChapterId: number; sourceId: number; imgVerticalId: number; imgHorizontalId: number; imgSquareId: number; imageVerticalId: number; imageHorizontalId: number; title: string; titleEn: string; fullTitle: string; fullTitleEn: string; description: string; slug: string; author: string; additionalInfo: string; importantInfo: string; status: string; views: number; likes: number; dislikes: number; intervalSec: number; intervalForNextFreeChapterSec: number; chapterCost: number; chapterPrice: number; chapterPriceCollected: number; maxSubscriptionChapters: number; maxDonateChapters: number; isDisplayOnMainPage: boolean; withoutSchedule: boolean; nextFreeChapterPublishedAt: null; createdAt: Date; updatedAt: Date; userRating: null; userBookmark: null; country: Country; genres: Genre[]; timer: Timer; chapters: ChapterElement[]; verticalImage: Image; horizontalImage: Image; squareImage: Image; } interface ChapterElement { id: number; bookId: number; title: string; url: string; numberChapter: string; tom: number | null; chapterShortNumber: number | null; isDonate: boolean; isSponsored: boolean; isSubscription: boolean; isEdited: boolean; publishedAt: Date; price: number; views: number; isUserPaid?: boolean; } interface Country { id: number; title: string; code: string; } interface Genre { id: number; title: string; slug: string; } interface Image { id: number; alt: string; path: string; name: string; url: string; } interface Timer { value: null; for: null; message: string; } interface PagePropsChapter { id: number; bookId: number; editorId: null; translatorId: number; parserChapterId: null; title: string; slug: string; status: string; tom: null; views: number; isSponsored: boolean; isSubscription: boolean; isDonate: boolean; isIndexing: boolean; publishedAt: Date; changeAvailabilityAt: null; createdAt: Date; updatedAt: Date; numberChapter: string; price: number; content: Content; book: ChapterBook; userBookmark: null; isUserPaid: boolean; nextChapter: null; previousChapter: PreviousChapter; corrections: null; } interface ChapterBook { id: number; title: string; titleEn: string; url: string; } interface Content { id: number; text: string; symbolsCount: number; } interface PreviousChapter { id: number; title: string; views: number; publishedAt: Date; url: string; numberChapter: string; } interface InitialReduxState { data?: Data; auth: Auth; banners?: Banners; corrections?: Corrections; readerSettings?: ReaderSettings; } interface Auth { isAuth: boolean; user: null; } interface Banners { displaySale: boolean; } interface Corrections { items: null; data: null; } interface Data { bookPage: BookPage | null; chapterPage: ChapterPage | null; } interface BookPage { bookId: number; bookmark: null; } interface ChapterPage { chapterId: number; bookId: number; bookmark: null; } interface ReaderSettings { fontSize: number; lineHeight: number; textIndent: number; sidebarHide: number; textAlign: string; } interface TotalData { items: Item[]; _links: Links; pagesData: PagesData; } interface Links { self: First; first: First; last: First; next: First; } interface First { href: string; } interface Item { id: number; title: string; description: string; slug: string; url: string; status: string; dislikes: number; likes: number; userRating: null; lastChapter: ChapterElement; verticalImage: Image; horizontalImage: Image; squareImage: Image; } interface PagesData { totalCount: number; pageCount: number; currentPage: number; perPage: number; } interface Query { book?: string; chapter?: string; }