import { Plugin } from '@/types/plugin'; import { fetchApi } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { load as parseHTML } from 'cheerio'; class WTRLAB implements Plugin.PluginBase { id = 'WTRLAB'; name = 'WTR-LAB'; site = 'https://wtr-lab.com/'; version = '1.0.1'; icon = 'src/en/wtrlab/icon.png'; sourceLang = 'en/'; async popularNovels( page: number, { showLatestNovels, filters, }: Plugin.PopularNovelsOptions, ): Promise { let link = this.site + this.sourceLang + 'novel-list?'; link += `orderBy=${filters.order.value}`; link += `&order=${filters.sort.value}`; link += `&filter=${filters.storyStatus.value}`; link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy. if (showLatestNovels) { const response = await fetchApi(this.site + 'api/home/recent', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ page: page }), }); const recentNovel: JsonNovel = await response.json(); // Parse novels from JSON const novels: Plugin.NovelItem[] = recentNovel.data.map( (datum: Datum) => ({ name: datum.serie.data.title || '', cover: datum.serie.data.image, path: this.sourceLang + 'serie-' + datum.serie.raw_id + '/' + datum.serie.slug || '', }), ); return novels; } else { const body = await fetchApi(link).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = loadedCheerio('.serie-item') .map((index, element) => ({ name: loadedCheerio(element) .find('.title-wrap > a') .text() .replace(loadedCheerio(element).find('.rawtitle').text(), '') || '', cover: loadedCheerio(element).find('img').attr('src'), path: loadedCheerio(element).find('a').attr('href') || '', })) .get() .filter(novel => novel.name && novel.path); return novels; } } async parseNovel(novelPath: string): Promise { const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, name: loadedCheerio('h1.text-uppercase').text(), cover: loadedCheerio('.img-wrap > img').attr('src'), summary: loadedCheerio('.lead').text().trim(), }; novel.genres = loadedCheerio('td:contains("Genre")') .next() .find('a') .map((i, el) => loadedCheerio(el).text()) .toArray() .join(','); novel.author = loadedCheerio('td:contains("Author")') .next() .text() .replace(/[\t\n]/g, ''); novel.status = loadedCheerio('td:contains("Status")') .next() .text() .replace(/[\t\n]/g, ''); const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ''; const jsonData: NovelJson = JSON.parse(chapterJson); const chapters: Plugin.ChapterItem[] = jsonData.props.pageProps.serie.chapters.map( (jsonChapter, chapterIndex) => ({ name: jsonChapter.title, path: this.sourceLang + 'serie-' + jsonData.props.pageProps.serie.serie_data.raw_id + '/' + jsonData.props.pageProps.serie.serie_data.slug + '/chapter-' + jsonChapter.order, // Assuming 'slug' is the intended path releaseTime: ( jsonChapter?.created_at || jsonChapter?.updated_at )?.substring(0, 10), chapterNumber: chapterIndex + 1, }), ); novel.chapters = chapters; return novel; } async parseChapter(chapterPath: string): Promise { const body = await fetchApi(this.site + chapterPath).then(res => res.text(), ); const loadedCheerio = parseHTML(body); const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ''; const jsonData: NovelJson = JSON.parse(chapterJson); const chapterContent = JSON.stringify( jsonData.props.pageProps.serie.chapter_data.data.body, ); const parsedArray = JSON.parse(chapterContent); let htmlString = ''; for (const text of parsedArray) { htmlString += `

${text}

`; } return htmlString; } async searchNovels(searchTerm: string): Promise { const response = await fetchApi(this.site + 'api/search', { headers: { 'Content-Type': 'application/json', Referer: this.site + this.sourceLang, Origin: this.site, }, method: 'POST', body: JSON.stringify({ text: searchTerm }), }); const recentNovel: JsonNovel = await response.json(); // Parse novels from JSON const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({ name: datum.data.title || '', cover: datum.data.image, path: this.sourceLang + 'serie-' + datum.raw_id + '/' + datum.slug || '', })); return novels; } filters = { order: { value: 'chapter', label: 'Order by', options: [ { label: 'View', value: 'view' }, { label: 'Name', value: 'name' }, { label: 'Addition Date', value: 'date' }, { label: 'Reader', value: 'reader' }, { label: 'Chapter', value: 'chapter' }, ], type: FilterTypes.Picker, }, sort: { value: 'desc', label: 'Sort by', options: [ { label: 'Descending', value: 'desc' }, { label: 'Ascending', value: 'asc' }, ], type: FilterTypes.Picker, }, storyStatus: { value: 'all', label: 'Status', options: [ { label: 'All', value: 'all' }, { label: 'Ongoing', value: 'ongoing' }, { label: 'Completed', value: 'completed' }, ], type: FilterTypes.Picker, }, } satisfies Filters; } type NovelJson = { props: Props; page: string; }; type Props = { pageProps: PageProps; __N_SSP: boolean; }; type PageProps = { serie: Serie; server_time: Date; }; type Serie = { serie_data: SerieData; chapters: Chapter[]; recommendation: SerieData[]; chapter_data: ChapterData; id: number; raw_id: number; slug: string; data: Data; is_default: boolean; raw_type: string; }; type Chapter = { serie_id: number; id: number; order: number; slug: string; title: string; name: string; created_at: string; updated_at: string; }; type ChapterData = { data: ChapterContent; }; type ChapterContent = { title: string; body: string; }; type SerieData = { serie_id?: number; recommendation_id?: number; score?: string; id: number; slug: string; search_text: string; status: number; data: Data; created_at: string; updated_at: string; view: number; in_library: number; rating: number | null; chapter_count: number; power: number; total_rate: number; user_status: number; verified: boolean; from: null; raw_id: number; genres?: number[]; }; type Data = { title: string; author: string; description: string; image: string; }; type JsonNovel = { success: boolean; data: Datum[]; }; type Datum = { serie: Serie; chapters: Chapter[]; updated_at: Date; raw_id: number; slug: string; data: Data; }; export default new WTRLAB();