import { Parser } from 'htmlparser2';
import { fetchApi } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
class RoyalRoad implements Plugin.PluginBase {
id = 'royalroad';
name = 'Royal Road';
version = '2.1.2';
icon = 'src/en/royalroad/icon.png';
site = 'https://www.royalroad.com/';
parseNovels(html: string) {
const novels: Plugin.NovelItem[] = [];
let tempNovel = {} as Plugin.NovelItem;
tempNovel.name = '';
let isParsingNovel = false;
let isNovelName = false;
const parser = new Parser({
onopentag(name, attribs) {
if (attribs['class']?.includes('fiction-list-item')) {
isParsingNovel = true;
}
if (isParsingNovel) {
if (name === 'a' && attribs['class']?.includes('bold')) {
tempNovel.path = attribs['href'].slice(1);
isNovelName = true;
}
if (name === 'img') {
tempNovel.cover = attribs['src'];
}
if (tempNovel.path && tempNovel.name) {
novels.push(tempNovel);
tempNovel = {} as Plugin.NovelItem;
tempNovel.name = '';
}
}
},
ontext(data) {
if (isNovelName) {
tempNovel.name += data;
}
},
onclosetag(name) {
if (name === 'h2') {
isNovelName = false;
isParsingNovel = false;
}
},
});
parser.write(html);
parser.end();
return novels;
}
async popularNovels(
page: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions,
): Promise {
let link = `${this.site}fictions/search`;
link += `?page=${page}`;
if (!filters) filters = this.filters || {};
if (showLatestNovels) link += '&orderBy=last_update';
for (const key in filters) {
if (filters[key as keyof typeof filters].value === '') continue;
if (key === 'genres' || key === 'tags' || key === 'content_warnings') {
if (filters[key].value.include)
for (const include of filters[key].value.include) {
link += `&tagsAdd=${include}`;
}
if (filters[key].value.exclude)
for (const exclude of filters[key].value.exclude) {
link += `&tagsRemove=${exclude}`;
}
} else if (typeof filters[key as keyof typeof filters] === 'object')
link += `&${key}=${filters[key as keyof typeof filters].value}`;
}
const body = await fetchApi(link).then(r => r.text());
return this.parseNovels(body);
}
async parseNovel(novelPath: string): Promise {
const result = await fetchApi(this.site + novelPath);
const html = await result.text();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: '',
summary: '',
chapters: [],
};
let isNovelName = false;
let isAuthorName = false;
let isDescription = false;
let isH4 = false;
let isSpan = 0;
let isTags = false;
let isGenres = false;
const genreArray: string[] = [];
let isFooter = false;
let isScript = false;
let chapterJson: ChapterEntry[] = [];
let volumeJson: VolumeEntry[] = [];
const parser = new Parser({
onopentag(name, attribs) {
if (name === 'img' && attribs['class']?.includes('thumbnail')) {
novel.cover = attribs['src'];
}
if (name === 'span' && attribs['class']?.includes('label-sm')) {
isSpan++;
}
if (name === 'span' && attribs['class']?.includes('tags')) {
isTags = true;
}
},
onopentagname(name) {
if (name === 'h1') {
isNovelName = true;
}
if (isH4 && name === 'a') {
isAuthorName = true;
}
if (isTags && name === 'a') {
isGenres = true;
}
if (name === 'label') {
isDescription = false;
isTags = false;
}
if (isFooter && name === 'script') {
isScript = true;
}
},
onattribute(name, value) {
if (name === 'class' && value === 'description') {
isDescription = true;
}
if (name === 'class' && value === 'page-footer footer') {
isFooter = true;
}
},
ontext(data) {
if (isNovelName) {
novel.name = novel.name + data;
}
if (isAuthorName) {
novel.author = data;
isAuthorName = false;
}
if (isDescription) {
novel.summary += data;
}
if (isSpan === 2) {
novel.status = data.trim();
isSpan++;
}
if (isGenres) {
genreArray.push(data);
}
if (isScript) {
if (data.includes('window.chapters =')) {
chapterJson = JSON.parse(
data.match(/window.chapters = (.+])(?=;)/)![1],
);
volumeJson = JSON.parse(
data.match(/window.volumes = (.+])(?=;)/)![1],
);
}
}
},
onclosetag(name) {
if (name === 'h1') {
isNovelName = false;
isH4 = true;
}
if (name === 'h4') {
isH4 = false;
}
if (name === 'a') {
isGenres = false;
}
if (name === 'script') {
isScript = false;
}
if (name === 'body') {
isFooter = false;
}
},
});
parser.write(html);
parser.end();
novel.summary = novel.summary?.trim();
novel.genres = genreArray.join(', ');
switch (novel.status) {
case 'ONGOING':
novel.status = NovelStatus.Ongoing;
break;
case 'HIATUS':
novel.status = NovelStatus.OnHiatus;
break;
case 'COMPLETED':
novel.status = NovelStatus.Completed;
break;
default:
novel.status = NovelStatus.Unknown;
}
const chapter: Plugin.ChapterItem[] = chapterJson.map(
(chapter: ChapterEntry) => {
const matchingVolume = volumeJson.find(
(volume: VolumeEntry) => volume.id === chapter.volumeId,
);
return {
name: chapter.title,
path: chapter.url.slice(1),
releaseTime: chapter.date,
chapterNumber: chapter?.order,
page: matchingVolume?.title,
};
},
);
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise {
const result = await fetchApi(this.site + chapterPath);
const html = await result.text();
const parts: string[] = [];
const regexPatterns: RegExp[] = [
/