diff --git a/public/static/src/en/reaperscans/icon.png b/public/static/src/en/reaperscans/icon.png new file mode 100644 index 0000000..ee8eeb5 Binary files /dev/null and b/public/static/src/en/reaperscans/icon.png differ diff --git a/src/plugins/english/reaperscans.ts b/src/plugins/english/reaperscans.ts new file mode 100644 index 0000000..2aa72fe --- /dev/null +++ b/src/plugins/english/reaperscans.ts @@ -0,0 +1,126 @@ +import { fetchApi } from '@libs/fetch'; +import type { Plugin } from '@typings/plugin'; + +const API_BASE = 'https://api.reaperscans.com'; +const MEDIA_BASE = 'https://media.reaperscans.com/file/4SRBHm/'; + +interface ApiResponse { + data: T; + meta: { + total: number; + per_page: number; + current_page: number; + last_page: number; + }; +} + +interface ReaperNovel { + title: string; + thumbnail: string; + series_slug: string; +} + +class ReaperScans implements Plugin.PluginBase { + id = 'reaperscans.com'; + name = 'Reaper Scans'; + version = '1.0.0'; + icon = 'src/en/reaperscans/icon.png'; + site = 'https://reaperscans.com'; + + async popularNovels(page: number): Promise { + return this.query(page); + } + + async parseNovel(novelPath: string): Promise { + const novelResp = await fetchApi(`${API_BASE}/series/${novelPath}`); + const novel: { + title: string; + thumbnail: string; + description: string; + tags: string[]; + rating: number; + status: string; + alternative_names: string; + author: string; + studio: string; + } = await novelResp.json(); + + const chaptersResp = await fetchApi( + `${API_BASE}/chapters/${novelPath}?perPage=${Number.MAX_SAFE_INTEGER}`, + ); + const chapters: { + chapter_slug: string; + chapter_name: string; + index: string; + created_at: string; + }[] = (await chaptersResp.json()).data; + + return { + name: novel.title, + cover: this.getCoverUrl(novel.thumbnail), + author: novel.author, + artist: novel.studio, + status: novel.status, + rating: novel.rating, + summary: novel.description, + genres: novel.tags.join(','), + path: novelPath, + chapters: chapters.reverse().map(chapter => ({ + path: `${novelPath}/${chapter.chapter_slug}`, + name: chapter.chapter_name, + chapterNumber: Number.parseFloat(chapter.index), + releaseTime: chapter.created_at.substring(0, 10), + })), + }; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(`${this.site}/series/${chapterPath}`, { + headers: { RSC: '1' }, + }); + const body = await result.text(); + return this.extractChapterContent(body); + } + + private extractChapterContent(chapter: string): string { + const content = chapter + .split('\n') + .find(e => e.substring(0, 50).includes('') + 1, + ); + } + + async searchNovels(searchTerm: string): Promise { + return this.query(1, searchTerm); + } + + private async query(page = 1, search = ''): Promise { + const link = `${API_BASE}/query?page=${page}&perPage=20&series_type=Novel&query_string=${search}&order=desc&orderBy=created_at&adult=true&status=All&tags_ids=[]`; + const result = await fetchApi(link); + const json: ApiResponse = await result.json(); + + return json.data.map(novel => ({ + name: novel.title, + cover: novel.thumbnail.startsWith('novels/') + ? MEDIA_BASE + novel.thumbnail + : novel.thumbnail, + path: novel.series_slug, + })); + } + + private getCoverUrl(thumbnail: string): string { + return thumbnail.startsWith('novels/') ? MEDIA_BASE + thumbnail : thumbnail; + } +} + +export default new ReaperScans();