import { CheerioAPI, load as parseHTML } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import dayjs from 'dayjs'; class ScribbleHubPlugin implements Plugin.PluginBase { id = 'scribblehub'; name = 'Scribble Hub'; icon = 'src/en/scribblehub/icon.png'; site = 'https://www.scribblehub.com/'; version = '1.0.2'; parseNovels(loadedCheerio: CheerioAPI) { const novels: Plugin.NovelItem[] = []; loadedCheerio('.search_main_box').each((i, el) => { const novelName = loadedCheerio(el).find('.search_title > a').text(); const novelCover = loadedCheerio(el) .find('.search_img > img') .attr('src'); const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href'); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, ''), }; novels.push(novel); }); return novels; } async popularNovels( page: number, { showLatestNovels, filters, }: Plugin.PopularNovelsOptions, ): Promise { let url = `${this.site}`; if (showLatestNovels) { url += `latest-series/?pg=${page}`; } else if (filters) { const params = new URLSearchParams(); if (filters.genres.value.include?.length) { params.append('gi', filters.genres.value.include.join(',')); } if ( filters.genres.value.include?.length || filters.genres.value.exclude?.length ) { params.append('mgi', filters.genre_operator.value); } if (filters.genres.value.exclude?.length) { params.append('ge', filters.genres.value.exclude.join(',')); } if (filters.content_warning.value.include?.length) { params.append('cti', filters.content_warning.value.include.join(',')); } if ( filters.content_warning.value.include?.length || filters.content_warning.value.exclude?.length ) { params.append('mct', filters.content_warning_operator.value); } if (filters.content_warning.value.exclude?.length) { params.append('cte', filters.content_warning.value.exclude.join(',')); } params.append('cp', filters.storyStatus.value); params.append('sort', filters.sort.value); params.append('order', filters.order.value); params.append('pg', page.toString()); url += `series-finder/?sf=1&${params.toString()}`; } else { url += `series-finder/?sf=1&sort=ratings&order=desc&pg=${page}`; } const body = await fetchApi(url).then(result => result.text()); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } async parseNovel(novelPath: string): Promise { const result = await fetchApi(this.site + novelPath); const body = await result.text(); let loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, name: loadedCheerio('.fic_title').text() || 'Untitled', cover: loadedCheerio('.fic_image > img').attr('src'), summary: loadedCheerio('.wi_fic_desc').text(), author: loadedCheerio('.auth_name_fic').text(), chapters: [], }; novel.genres = loadedCheerio('.fic_genre') .map((i, el) => loadedCheerio(el).text()) .toArray() .join(','); novel.status = loadedCheerio('.rnd_stats').next().text().includes('Ongoing') ? 'Ongoing' : 'Completed'; const formData = new FormData(); formData.append('action', 'wi_getreleases_pagination'); formData.append('pagenum', '-1'); formData.append('mypostid', novelPath.split('/')[1]); const data = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, { method: 'POST', body: formData, }); const text = await data.text(); loadedCheerio = parseHTML(text); const chapter: Plugin.ChapterItem[] = []; const parseISODate = (date: string) => { 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 } return dayJSDate.toISOString(); } return null; }; loadedCheerio('.toc_w').each((i, el) => { const chapterName = loadedCheerio(el).find('.toc_a').text(); const releaseDate = loadedCheerio(el).find('.fic_date_pub').text(); const chapterUrl = loadedCheerio(el).find('a').attr('href'); if (!chapterUrl) return; chapter.push({ name: chapterName, releaseTime: parseISODate(releaseDate), path: chapterUrl.replace(this.site, ''), }); }); novel.chapters = chapter.reverse(); return novel; } async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); const chapterText = loadedCheerio('div.chp_raw').html() || ''; return chapterText; } async searchNovels(searchTerm: string): Promise { const url = `${this.site}?s=${encodeURIComponent(searchTerm)}&post_type=fictionposts`; const result = await fetchApi(url); const body = await result.text(); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } filters = { sort: { label: 'Sort Results By', value: 'ratings', options: [ { label: 'Chapters', value: 'chapters' }, { label: 'Chapters per Week', value: 'frequency' }, { label: 'Date Added', value: 'dateadded' }, { label: 'Favorites', value: 'favorites' }, { label: 'Last Updated', value: 'lastchdate' }, { label: 'Number of Ratings', value: 'numofrate' }, { label: 'Pages', value: 'pages' }, { label: 'Pageviews', value: 'pageviews' }, { label: 'Ratings', value: 'ratings' }, { label: 'Readers', value: 'readers' }, { label: 'Reviews', value: 'reviews' }, { label: 'Total Words', value: 'totalwords' }, ], type: FilterTypes.Picker, }, order: { label: 'Order By', value: 'desc', options: [ { label: 'Descending', value: 'desc' }, { label: 'Ascending', value: 'asc' }, ], type: FilterTypes.Picker, }, storyStatus: { label: 'Story Status', value: 'all', options: [ { label: 'All', value: 'all' }, { label: 'Completed', value: 'completed' }, { label: 'Ongoing', value: 'ongoing' }, { label: 'Hiatus', value: 'hiatus' }, ], type: FilterTypes.Picker, }, genre_operator: { value: 'and', label: 'Genres (And/Or)', options: [ { label: 'And', value: 'and' }, { label: 'Or', value: 'or' }, ], type: FilterTypes.Picker, }, genres: { label: 'Genres', value: { include: [], exclude: [], }, options: [ { label: 'Action', value: '9' }, { label: 'Adult', value: '902' }, { label: 'Adventure', value: '8' }, { label: 'Boys Love', value: '891' }, { label: 'Comedy', value: '7' }, { label: 'Drama', value: '903' }, { label: 'Ecchi', value: '904' }, { label: 'Fanfiction', value: '38' }, { label: 'Fantasy', value: '19' }, { label: 'Gender Bender', value: '905' }, { label: 'Girls Love', value: '892' }, { label: 'Harem', value: '1015' }, { label: 'Historical', value: '21' }, { label: 'Horror', value: '22' }, { label: 'Isekai', value: '37' }, { label: 'Josei', value: '906' }, { label: 'LitRPG', value: '1180' }, { label: 'Martial Arts', value: '907' }, { label: 'Mature', value: '20' }, { label: 'Mecha', value: '908' }, { label: 'Mystery', value: '909' }, { label: 'Psychological', value: '910' }, { label: 'Romance', value: '6' }, { label: 'School Life', value: '911' }, { label: 'Sci-fi', value: '912' }, { label: 'Seinen', value: '913' }, { label: 'Slice of Life', value: '914' }, { label: 'Smut', value: '915' }, { label: 'Sports', value: '916' }, { label: 'Supernatural', value: '5' }, { label: 'Tragedy', value: '901' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, content_warning_operator: { value: 'and', label: 'Mature Content (And/Or)', options: [ { label: 'And', value: 'and' }, { label: 'Or', value: 'or' }, ], type: FilterTypes.Picker, }, content_warning: { value: { include: [], exclude: [], }, label: 'Mature Content', options: [ { label: 'Gore', value: '48' }, { label: 'Sexual Content', value: '50' }, { label: 'Strong Language', value: '49' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, } satisfies Filters; } export default new ScribbleHubPlugin();