import { CheerioAPI, load as parseHTML } from "cheerio"; import { fetchApi, fetchFile } from "@libs/fetch"; import { Plugin } from "@typings/plugin"; import { Filters } from "@libs/filterInputs"; import { NovelStatus } from "@libs/novelStatus"; import { NovelItem } from "../../test_web/static/js"; interface SearchedNovel { name: string; slug: string; coverUrl: string; } interface SearchedResult { data?: SearchedNovel[] } class LightNovelVN implements Plugin.PagePlugin { id = "lightnovel.vn"; name = "Light Novel VN"; version = "1.0.0"; icon = "src/vi/lightnovelvn/icon.png"; filters?: Filters | undefined; site = "https://lightnovel.vn"; async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { const url = `${this.site}/truyen-hot-ds?page=${pageNo}`; const body = await fetch(url).then((r) => r.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => { const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim(); const img = loadedCheerio(ele).find('noscript').html(); const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1]; const novelUrl = loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href'); if(novelUrl) novels.push({ name: novelName, cover: novelCover, path: novelUrl.replace(this.site, ''), }); }); return novels; } parseChapters(loadedCheerio: CheerioAPI){ const chapters: Plugin.ChapterItem[] = []; loadedCheerio('ul.chapter-list > li').each((idx, ele) => { const chNum = Number(loadedCheerio(ele).find('div').first().text()); const chapterUrl = loadedCheerio(ele).find('a').attr('href'); const name = loadedCheerio(ele).find('a').text().trim(); if(chapterUrl){ chapters.push({ path: chapterUrl.replace(this.site, ''), name, chapterNumber: chNum }) } }) return chapters; } async parseNovel(novelPath: string): Promise { const url = this.site + novelPath; const body = await fetch(url).then((r) => r.text()); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel & { totalPages: number; } = { path: novelPath, chapters: [], name: 'Không có tiêu đề', totalPages: 1 }; novel.name = loadedCheerio('h1[itemprop="name"]').text().trim(); novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0] const genres: string[] = []; loadedCheerio('a[itemprop="genre"]').each(function() { genres.push(loadedCheerio(this).text()); }) novel.genres = genres.join(','); novel.status = loadedCheerio('span.font-bold.text-size22:last').text(); if(novel.status === 'Đang ra'){ novel.status = NovelStatus.Ongoing; }else if(novel.status === 'Hoàn thành'){ novel.status = NovelStatus.Completed; }else { novel.status = NovelStatus.Unknown; } novel.author = loadedCheerio('a[itemprop="author"] > span').text(); novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' '); const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); await delay(1000); const chapterListUrl = url + '/danh-sach-chuong'; const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); const loadedChapterList = parseHTML(chapterListBody); novel.chapters = this.parseChapters(loadedChapterList); loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => { const href = ele.attribs['href']; if(href){ const page = Number(href.match(/\?page=(\d+)/)?.[1]); if(page && page > novel.totalPages){ novel.totalPages = page; } } }) return novel; } async parsePage(novelPath: string, page: string): Promise { const url = `${this.site}${novelPath}/danh-sach-chuong?page=${page}`; const chapterListBody = await fetchApi(url).then(r => r.text()); const chapters = this.parseChapters(parseHTML(chapterListBody)); return { chapters } } async parseChapter(chapterPath: string): Promise { const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); const loadedCheerio = parseHTML(body); let chapterText = loadedCheerio("div.chapter-content").html() || ''; return chapterText; } async searchNovels(searchTerm: string, pageNo: number): Promise { if(pageNo > 1) return []; const url = `${this.site}/api/book-search`; let formData = new FormData(); formData.append("keyword", searchTerm); const result: SearchedResult = await fetchApi( url, { method: "POST", body: formData, }, ).then((r) => r.json()); const novels: NovelItem[] = result.data?.map(item => { return { name: item.name, path: '/truyen/' + item.slug, cover: (this.site + item.coverUrl).replace('default.jpg', '150.jpg?w=256&q=') } }) || []; return novels; } async fetchImage(url: string): Promise { return fetchFile(url); } } export default new LightNovelVN();