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[] = [ /