diff --git a/plugins/english/peachpuff.ts b/plugins/english/peachpuff.ts new file mode 100644 index 0000000..3118b65 --- /dev/null +++ b/plugins/english/peachpuff.ts @@ -0,0 +1,157 @@ +import { Plugin } from '@/types/plugin'; +import { fetchApi } from '@libs/fetch'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; + +class PeachPuffTranslations implements Plugin.PluginBase { + id = 'peachpuff'; + name = 'Peach Puff Translations'; + site = 'https://peachpuff.in/'; + version = '1.0.0'; + icon = 'src/english/peachpuff/icon.png'; + + coversCache?: Map; + + cleanCover(src?: string): string | undefined { + if (!src) return undefined; + return src + .replace('i0.wp.com/', '') + .replace(/^http:/, 'https:') + .split('?')[0]; + } + + novelPath(href: string | undefined): string | undefined { + if (!href) return undefined; + return href.replace(this.site, '').replace(/\/+$/, ''); + } + + async getNovelCovers(): Promise> { + if (this.coversCache) return this.coversCache; + const covers = new Map(); + try { + const [pages, media] = await Promise.all([ + fetchApi( + `${this.site}wp-json/wp/v2/pages?per_page=100&_fields=id,link`, + ).then(res => res.json()), + fetchApi( + `${this.site}wp-json/wp/v2/media?per_page=100&_fields=id,post,source_url`, + ).then(res => res.json()), + ]); + const pageLinks = new Map( + (pages as { id: number; link: string }[]).map(page => [ + page.id, + page.link, + ]), + ); + const mediaItems = media as { + id: number; + post?: number; + source_url: string; + }[]; + mediaItems.sort((a, b) => a.id - b.id); + for (const item of mediaItems) { + const path = item.post + ? this.novelPath(pageLinks.get(item.post)) + : undefined; + if (path && !covers.has(path)) covers.set(path, item.source_url); + } + } catch { + this.coversCache = covers; + return covers; + } + this.coversCache = covers; + return covers; + } + + parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('ul.wp-block-list li a[title]').each((_, element) => { + const path = this.novelPath(loadedCheerio(element).attr('href')); + if (!path) return; + const name = + loadedCheerio(element).attr('title') || + loadedCheerio(element).text().trim(); + novels.push({ name, path }); + }); + return novels; + } + + async popularNovels(): Promise { + const body = await fetchApi(this.site).then(res => res.text()); + const novels = this.parseNovels(parseHTML(body)); + const covers = await this.getNovelCovers(); + for (const novel of novels) novel.cover = covers.get(novel.path); + return novels; + } + + async searchNovels(searchTerm: string): Promise { + const query = searchTerm.toLowerCase(); + return (await this.popularNovels()).filter(novel => + novel.name.toLowerCase().includes(query), + ); + } + + 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('div.entry-title h2').first().text().trim(), + }; + + novel.cover = this.cleanCover( + loadedCheerio('figure.wp-block-image img').first().attr('src'), + ); + + loadedCheerio('p.wp-block-paragraph strong').each((_, element) => { + const key = loadedCheerio(element).text().trim().toLowerCase(); + if (key === 'author:') { + novel.author = loadedCheerio(element).nextUntil('strong').text().trim(); + } + }); + + const descriptionLabel = loadedCheerio( + 'p.wp-block-paragraph strong:contains("Description:")', + ).first(); + if (descriptionLabel.length) { + const summary = descriptionLabel + .parent() + .nextUntil('h4.wp-block-heading') + .map((_, element) => { + const paragraph = loadedCheerio(element); + paragraph.find('br').replaceWith('\n'); + return paragraph.text().trim(); + }) + .get() + .filter(Boolean) + .join('\n\n'); + if (summary) novel.summary = summary; + } + + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('.lcp_catlist li a').each((_, element) => { + const path = this.novelPath(loadedCheerio(element).attr('href')); + if (!path) return; + chapters.push({ + name: loadedCheerio(element).text().trim(), + path, + }); + }); + 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); + loadedCheerio('.category-post-dropdown-container').remove(); + loadedCheerio('script, style').remove(); + + return loadedCheerio('.entry-content').html() ?? ''; + } +} + +export default new PeachPuffTranslations(); diff --git a/public/static/src/english/peachpuff/icon.png b/public/static/src/english/peachpuff/icon.png new file mode 100644 index 0000000..67a0dbb Binary files /dev/null and b/public/static/src/english/peachpuff/icon.png differ