import { load as parseHTML } from 'cheerio'; import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; import { fetchApi, fetchFile } from '@libs/fetch'; import { Plugin } from '@typings/plugin'; import { Filters, FilterTypes } from '@libs/filterInputs'; import dayjs from 'dayjs'; class ReadLiteNovel implements Plugin.PluginBase { id = 'rln.app'; name = 'ReadLiteNovel'; version = '1.0.0'; icon = 'src/en/readlitenovel/icon.png'; site = 'https://rln.app'; parseAgoDate(date: string | undefined) { //parseMadaraDate if (date?.includes('ago')) { const dayJSDate = dayjs(new Date()); // today const timeAgo = date.match(/\d+/)?.[0] || ''; const timeAgoInt = parseInt(timeAgo, 10); if (!timeAgo) return null; // there is no number! if (date.includes('hours ago') || date.includes('hour ago')) { dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours } if (date.includes('days ago') || date.includes('day ago')) { dayJSDate.subtract(timeAgoInt, 'days'); // go back N days } if (date.includes('months ago') || date.includes('month ago')) { dayJSDate.subtract(timeAgoInt, 'months'); // go back N months } if (date.includes('years ago') || date.includes('year ago')) { dayJSDate.subtract(timeAgoInt, 'years'); // go back N years } return dayJSDate.toISOString(); } return null; // there is no "ago" so give up } async popularNovels( page: number, { filters }: Plugin.PopularNovelsOptions, ): Promise { const link = `${this.site}/ranking/${filters.order.value}/${page}`; const result = await fetchApi(link); const body = await result.text(); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; loadedCheerio('.category-items li').each((i, el) => { const novelUrl = loadedCheerio(el).find('.category-name a').attr('href'); if (!novelUrl) return; const novelName = loadedCheerio(el) .find('.category-name a') .text() .trim(); let novelCover = loadedCheerio(el).find('.category-img img').attr('src'); if (novelCover && !isUrlAbsolute(novelCover)) { novelCover = this.site + novelCover; } const novel = { path: novelUrl?.replace(this.site, ''), name: novelName, cover: novelCover, }; novels.push(novel); }); return novels; } async parseNovel(novelPath: string): Promise { const result = await fetchApi(this.site + novelPath); const body = await result.text(); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, name: loadedCheerio('.novel-title').text() || 'Untitled', cover: loadedCheerio('.novels-detail img').attr('src'), summary: loadedCheerio('.empty-box').text().trim(), chapters: [], }; loadedCheerio('.novels-detail-right li').each((i, el) => { const detailName = loadedCheerio(el).find('div:first').text(); const detail = loadedCheerio(el).find('div:last'); switch (detailName) { case 'Status:': novel.status = detail.text(); break; case 'Genres:': novel.genres = detail .find('a') .map((i, el) => loadedCheerio(el).text()) .toArray() .join(', ') || detail.text(); break; case 'Author(s):': novel.author = detail .find('a') .map((i, el) => loadedCheerio(el).text()) .toArray() .join(', ') || detail.text(); break; case 'Translator:': novel.artist = detail.text(); break; } }); const chapter: Plugin.ChapterItem[] = []; loadedCheerio('.cm-tabs-content li').each((i, el) => { const chapterUrl = loadedCheerio(el).find('a').attr('href'); if (!chapterUrl) return; const chapterName = loadedCheerio(el).find('a').text().trim(); const releaseDate = this.parseAgoDate( loadedCheerio(el).find('svg').attr('data-bs-original-title')!, ); chapter.push({ name: chapterName, path: chapterUrl?.replace(this.site, ''), releaseTime: releaseDate, }); }); novel.chapters = chapter; return novel; } async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); const chapterText = loadedCheerio('#chapterText').html() || ''; return chapterText; } async searchNovels( searchTerm: string, pageNo: number, ): Promise { const url = this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`; const result = await fetchApi(url); const body = await result.json(); const novels: Plugin.NovelItem[] = []; body.results.forEach( (item: { link: string; original_title: string; image: string }) => novels.push({ path: item.link, name: item.original_title, cover: item.image, }), ); return novels; } async fetchImage(url: string): Promise { return fetchFile(url); } filters = { order: { value: 'top-rated', label: 'Order by', options: [ { label: 'MOST VIEWED', value: 'most-viewed' }, { label: 'TOP RATED', value: 'top-rated' }, { label: 'BOOKMARKS', value: 'subscribers' }, { label: 'NEW', value: 'new' }, ], type: FilterTypes.Picker, }, } satisfies Filters; } export default new ReadLiteNovel();