import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; import { Filters, FilterTypes } from "@libs/filterInputs"; class NovelFull implements Plugin.PluginBase { id = "novelfull"; name = "NovelFull"; version = "1.0.0"; icon = "src/en/novelfull/icon.png"; site = "https://novelfull.com/"; parseNovels(loadedCheerio:CheerioAPI){ const novels: Plugin.NovelItem[] = []; loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => { const novelName = loadedCheerio(ele) .find("h3.truyen-title > a") .text(); const novelCover = this.site + loadedCheerio(ele).find("img").attr("src")?.slice(1); const novelUrl = loadedCheerio(ele) .find("h3.truyen-title > a") .attr("href") ?.slice(1); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, path: novelUrl, }; novels.push(novel); }); return novels; } async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise { let link = this.site; if (filters.genre.value !== '') link += `genre/${filters.genre.value}`; else link += filters.sort.value link += `?page=${page}`; const body = await fetchApi(link).then((r) => r.text()); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } async parseNovel(novelPath: string): Promise { const result = await fetchApi(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, name: loadedCheerio("div.book > img").attr("alt") || "Untitled", cover: this.site + loadedCheerio("div.book > img").attr("src"), summary: loadedCheerio("div.desc-text").text().trim(), status: loadedCheerio('h3:contains("Status")').next().text(), chapters: [], }; novel.author = loadedCheerio('h3:contains("Author")') .parent() .contents() .text() .replace("Author:", ""); novel.genres = loadedCheerio('h3:contains("Genre")') .siblings() .map((i, el) => loadedCheerio(el).text()) .toArray() .join(","); const novelId = loadedCheerio("#rating").attr("data-novel-id")!; const chapter: Plugin.ChapterItem[] = []; const getChapters = async (id: string) => { const chapterListUrl = this.site + "ajax/chapter-option?novelId=" + id; const data = await fetchApi(chapterListUrl); const chapterlist = await data.text(); loadedCheerio = parseHTML(chapterlist); loadedCheerio("select > option").each(function () { const chapterName = loadedCheerio(this).text(); const chapterUrl = loadedCheerio(this).attr("value")?.slice(1); if (!chapterUrl) return; chapter.push({ name: chapterName, path: chapterUrl, }); }); return chapter; } novel.chapters = await getChapters(novelId); return novel; } async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); loadedCheerio("#chapter-content div.ads").remove(); const chapterText = loadedCheerio("#chapter-content").html() || ''; return chapterText; } async searchNovels(searchTerm: string, page: number): Promise { const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`; const result = await fetchApi(searchUrl); const body = await result.text(); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } async fetchImage(url: string): Promise { return fetchFile(url); } filters = { sort: { value: "most-popular", label: "Sort by", options: [ { label: "Latest Release", value: "latest-release-novel" }, { label: "Hot Novel", value: "hot-novel" }, { label: "Completed Novel", value: "completed-novel" }, { label: "Most Popular", value: "most-popular" }, ], type: FilterTypes.Picker, }, genre: { value: "", label: "Genres", options: [ { label: "None", value: "" }, { label: "Shounen", value: "Shounen" }, { label: "Harem", value: "Harem" }, { label: "Comedy", value: "Comedy" }, { label: "Martial Arts", value: "Martial+Arts" }, { label: "School Life", value: "School+Life" }, { label: "Mystery", value: "Mystery" }, { label: "Shoujo", value: "Shoujo" }, { label: "Romance", value: "Romance" }, { label: "Sci-fi", value: "Sci-fi" }, { label: "Gender Bender", value: "Gender+Bender" }, { label: "Mature", value: "Mature" }, { label: "Fantasy", value: "Fantasy" }, { label: "Horror", value: "Horror" }, { label: "Drama", value: "Drama" }, { label: "Tragedy", value: "Tragedy" }, { label: "Supernatural", value: "Supernatural" }, { label: "Ecchi", value: "Ecchi" }, { label: "Xuanhuan", value: "Xuanhuan" }, { label: "Adventure", value: "Adventure" }, { label: "Action", value: "Action" }, { label: "Psychological", value: "Psychological" }, { label: "Xianxia", value: "Xianxia" }, { label: "Wuxia", value: "Wuxia" }, { label: "Historical", value: "Historical" }, { label: "Slice of Life", value: "Slice+of+Life" }, { label: "Seinen", value: "Seinen" }, { label: "Lolicon", value: "Lolicon" }, { label: "Adult", value: "Adult" }, { label: "Josei", value: "Josei" }, { label: "Sports", value: "Sports" }, { label: "Smut", value: "Smut" }, { label: "Mecha", value: "Mecha" }, { label: "Yaoi", value: "Yaoi" }, { label: "Shounen Ai", value: "Shounen+Ai" }, { label: "History", value: "History" }, { label: "Martial", value: "Martial" } ], type: FilterTypes.Picker, }, } satisfies Filters; } export default new NovelFull();