import { Parser } from 'htmlparser2'; import { fetchApi, fetchFile } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@typings/plugin'; class RanobesPlugin implements Plugin.PagePlugin { id = 'ranobes'; name = 'Ranobes'; icon = 'src/en/ranobes/icon.png'; site = 'https://ranobes.top'; filters?: Filters | undefined; version = '2.0.0'; async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } parseNovels(html: string) { const novels: Plugin.NovelItem[] = []; let tempNovel = {} as Plugin.NovelItem; tempNovel.name = ''; const baseUrl = this.site let isParsingNovel = false; let isTitleTag = false; let isNovelName = false; const parser = new Parser({ onopentag(name, attribs) { if (attribs['class']?.includes('short-cont')){ isParsingNovel = true; } if (isParsingNovel){ if (name === 'h2' && attribs['class']?.includes('title')){ isTitleTag = true; } if (isTitleTag && name === 'a'){ tempNovel.path = attribs['href'].slice(baseUrl.length); isNovelName = true; } if (name === 'figure'){ tempNovel.cover = attribs['style'] .replace(/.*url\((.*?)\)./g, '$1'); } if (tempNovel.path && tempNovel.cover){ novels.push(tempNovel); tempNovel = {} as Plugin.NovelItem; tempNovel.name = ''; } } }, ontext(data){ if(isNovelName){ tempNovel.name += data; } }, onclosetag(name){ if(name === 'h2'){ isNovelName = false; isTitleTag = false; } if(name === 'figure'){ isParsingNovel = false; } } }) parser.write(html); parser.end(); return novels; } parseChapters(data: { chapters: ChapterEntry[] }) { const chapter: Plugin.ChapterItem[] = []; data.chapters.map((item: ChapterEntry) => { chapter.push({ name: item.title, releaseTime: new Date(item.date).toISOString(), path: item.link.slice(this.site.length), }); }); return chapter.reverse(); } async popularNovels( page: number, { showLatestNovels, filters, }: Plugin.PopularNovelsOptions, ): Promise { let link = `${this.site}/novels/page/${page}/`; const body = await fetchApi(link).then(r => r.text()); return this.parseNovels(body); } async parseNovel( novelPath: string, ): Promise { const url = this.site + novelPath; const result = await fetchApi(url); const html = await result.text(); const novel: Plugin.SourceNovel & { totalPages: number } = { path: novelPath, name: '', summary: '', chapters: [], totalPages: 1, } let baseUrl = this.site; let isCover = false; let isAuthor = false; let isSummary = false; let isStatus = false; let isStatusText = false; let isGenres = false; let isGenresText = false; let genreArray: string[] = [] let novelId = ''; const parser = new Parser ({ onopentag(name,attribs){ if (attribs['class'] === 'poster'){ isCover = true; } if (isCover && name === 'img'){ novel.name = attribs['alt']; novel.cover = baseUrl + attribs['src']; } if (name === 'div' && attribs['class'] === 'moreless'){ isSummary = true; } if (name === 'li' && attribs['title']?.includes('Original status')){ isStatus = true; } if (name === 'input' && attribs['name'] === 'newsid'){ novelId = attribs['value'] } }, onopentagname(name){ if (isSummary && name === 'br'){ novel.summary += `/n`; } if (isStatus && name === 'a'){ isStatusText = true; } if (isGenres && name === 'a'){ isGenresText = true; } }, onattribute(name,value){ if (name === 'itemprop' && value === 'creator'){ isAuthor = true; } if (name === 'id' && value === 'mc-fs-genre'){ isGenres = true; } }, ontext(data){ if(isAuthor){ novel.author = data; } if(isSummary){ novel.summary += data; } if(isStatusText){ novel.status = data === 'Ongoing' ? 'Ongoing' : 'Completed'; } if(isGenresText){ genreArray.push(data); } }, onclosetag(name){ if(name === 'a'){ isCover = false; isAuthor = false; isStatusText = false; isGenresText = false; isStatus = false; } if(name === 'div'){ isSummary = false; isGenres = false; } } }) parser.write(html); parser.end(); novel.genres = genreArray.join(', '); const chapterListUrl = this.site + '/chapters/' + novelId + '/'; const chaptersHtml = await fetchApi(chapterListUrl).then(r => r.text(), ); let dataJson: { pages_count: string, chapters: ChapterEntry[] } = { pages_count: '', chapters : [] }; let isScript = false; const parser2 = new Parser ({ ontext(data){ if(isScript){ if (data.includes('window.__DATA__ =')){ dataJson = JSON.parse( data.replace('window.__DATA__ =', '') ); } } }, onclosetag(name){ if(name === 'main'){ isScript = true; } if (name === 'script'){ isScript = false; } }, }) parser2.write(chaptersHtml); parser2.end(); novel.totalPages = Number(dataJson.pages_count); novel.chapters = this.parseChapters(dataJson); const latestChapterUrl = dataJson.chapters[0].link; const latestChapterName = dataJson.chapters[0].title; const latestChapterDate = dataJson.chapters[0].date; novel.latestChapter = latestChapterUrl ? { path: latestChapterUrl.replace(this.site, ''), name: latestChapterName, releaseTime: new Date(latestChapterDate).toISOString(), } : undefined; return novel; } async parsePage(novelPath: string, page: string): Promise { const pagePath = novelPath.split('-')[0]; const firstUrl = this.site + '/chapters' + pagePath.replace('novels/', ''); const pageUrl = firstUrl + '/page/' + page; const pageBody = await fetchApi(pageUrl).then(r => r.text()); let isScript = false; let dataJson: { pages_count: string, chapters: ChapterEntry[] } = { pages_count: '', chapters : [] }; const parser = new Parser({ ontext(data){ if(isScript){ if (data.includes('window.__DATA__ =')){ dataJson = JSON.parse( data.replace('window.__DATA__ =', '') ); } } }, onclosetag(name){ if(name === 'main'){ isScript = true; } if (name === 'script'){ isScript = false; } } }) parser.write(pageBody); parser.end(); const chapters = this.parseChapters(dataJson); return { chapters, }; } async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + chapterPath); const html = await result.text(); let chapterText = ''; let isChapter = false; let isPtag = false; let isStyleText = false; const parser = new Parser({ onopentag(name,attribs){ if (isChapter && name === 'div'){ let stylediv = attribs['style'] if(stylediv){ chapterText += `
` isStyleText = true; } else { chapterText += `
` } } if (isChapter && name === 'table'){ let w = attribs['width']; if(w) { chapterText += ``; } else { chapterText += `
`; } } if (isChapter && name === 'tbody'){ chapterText += ``; } if (isChapter && name === 'tr'){ chapterText += ``; } if (isChapter && name === 'td'){ let w1 = attribs['width']; if(w1) { chapterText += ``; } if (isChapter && name === 'tr'){ chapterText += ``; } if (isChapter && name === 'tbody'){ chapterText += ``; } if (isChapter && name === 'table'){ chapterText += `
`; } else { chapterText += ``; } } }, onattribute(name, value){ if (name === 'id' && value === 'arrticle'){ isChapter = true; } if (name === 'class' && value === 'category grey ellipses'){ isChapter = false; isPtag = false; } }, onopentagname(name){ if (isChapter && name === 'p'){ chapterText += '

'; isPtag = true; if(isStyleText){ isStyleText = false; } } if (isChapter && name === 'br'){ chapterText += `
`; } }, ontext(data){ if(isPtag){ chapterText += data; } if(isStyleText){ chapterText += data; } }, onclosetag(name){ if (name === 'p'){ isPtag = false; chapterText += '

' } if (isChapter && name === 'td'){ chapterText += `
`; } if (isChapter && name === 'div'){ isStyleText = false; chapterText += `
` } } }) parser.write(html) parser.end(); return chapterText; } async searchNovels( searchTerm: string, page: number, ): Promise { let link = `${this.site}/search/${searchTerm}/page/${page}`; const body = await fetchApi(link).then(r => r.text()); return this.parseNovels(body); } async fetchImage(url: string): Promise { return await fetchFile(url); } } export default new RanobesPlugin(); interface ChapterEntry { id: number; title: string; date: string; link: string; }