import { load as parseHTML } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { Filters, FilterTypes } from '@libs/filterInputs'; import { defaultCover } from '@libs/defaultCover'; import dayjs from 'dayjs'; import { storage } from '@libs/storage'; class Novelight implements Plugin.PagePlugin { id = 'novelight'; name = 'Novelight'; version = '1.1.5'; icon = 'src/en/novelight/icon.png'; site = 'https://novelight.net/'; hideLocked = storage.get('hideLocked'); pluginSettings = { hideLocked: { value: '', label: 'Hide locked chapters', type: 'Switch', }, }; async popularNovels( pageNo: number, { showLatestNovels, filters, }: Plugin.PopularNovelsOptions, ): Promise { let url = `${this.site}catalog/`; if (showLatestNovels) { url += `?ordering=-time_updated&page=${pageNo}`; } else if (filters) { const params = new URLSearchParams(); for (const country of filters.country.value) { params.append('country', country); } for (const genre of filters.genres.value) { params.append('genre', genre); } for (const translation of filters.translation.value) { params.append('translation', translation); } for (const status of filters.status.value) { params.append('status', status); } for (const novel_type of filters.novel_type.value) { params.append('type', novel_type); } params.append('ordering', filters.sort.value); params.append('page', pageNo.toString()); url += `?${params.toString()}`; } else { url += `?&ordering=popularity&page=${pageNo}`; } const body = await fetchApi(url).then(r => r.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; loadedCheerio('a.item').each((idx, ele) => { const novelName = loadedCheerio(ele).find('div.title').text().trim(); const novelUrl = ele.attribs.href; const bareNovelCover = loadedCheerio(ele).find('img').attr('src'); const novelCover = bareNovelCover ? this.site + bareNovelCover : defaultCover; if (!novelUrl) return; const novel = { name: novelName, cover: novelCover ?? defaultCover, path: novelUrl.replace('/', ''), }; novels.push(novel); }); return novels; } async parseNovel( novelPath: string, ): Promise { const body = await fetchApi(this.site + novelPath).then(r => r.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel & { totalPages: number } = { path: novelPath, name: loadedCheerio('h1').text() || 'Untitled', cover: this.site + loadedCheerio('.poster > img').attr('src'), summary: loadedCheerio('section.text-info.section > p').text(), totalPages: loadedCheerio('#select-pagination-chapter > option').length, chapters: [], }; const info = loadedCheerio('div.mini-info > .item').toArray(); let status = ''; let translation = ''; for (const child of info) { const type = loadedCheerio(child).find('.sub-header').text().trim(); if (type === 'Status') { status = loadedCheerio(child) .find('div.info') .text() .trim() .toLowerCase(); } if (type === 'Translation') { translation = loadedCheerio(child) .find('div.info') .text() .trim() .toLowerCase(); } if (type === 'Author') { novel.author = loadedCheerio(child).find('div.info').text().trim(); } if (type === 'Genres') { novel.genres = loadedCheerio(child) .find('div.info > a') .map((i, el) => loadedCheerio(el).text()) .toArray() .join(', '); } } if (status === 'cancelled') novel.status = NovelStatus.Cancelled; else if (status === 'releasing' || translation === 'ongoing') novel.status = NovelStatus.Ongoing; else if (status === 'completed' && translation === 'completed') novel.status = NovelStatus.Completed; return novel; } async parsePage(novelPath: string, page: string): Promise { const rawBody = await fetchApi(this.site + novelPath).then(r => r.text()); const csrftoken = rawBody?.match(/window\.CSRF_TOKEN = "([^"]+)"/)?.[1]; const bookId = rawBody?.match(/const OBJECT_BY_COMMENT = ([0-9]+)/)?.[1]; const totalPages = parseInt( rawBody ?.match(/