Files
lnreader-plugins/plugins/english/readlitenovel.ts
T
Rajarshee Chatterjee 20c2b75b01 refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc

* feat: Overhaul Playground UI

* feat: Add Filters to New UI

* feat: Persist Selected Plugin

* feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts

[skip ci]

* feat: UX Improvements

* chore: Update Plugin Path in Publish Workflow

* fix: Add Backward Compatibility

* chore: Update Plugin Publishing Script
2025-10-20 22:54:08 +05:30

191 lines
5.6 KiB
TypeScript

import { load as parseHTML } from 'cheerio';
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
import { fetchApi } from '@libs/fetch';
import { Plugin } from '@/types/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import dayjs from 'dayjs';
class ReadLiteNovel implements Plugin.PluginBase {
id = 'rln.app';
name = 'ReadLiteNovel';
version = '1.0.1';
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<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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): Promise<Plugin.NovelItem[]> {
const url =
this.site +
`/search/autocomplete?dataType=json&query=${encodeURIComponent(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;
}
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();