Reaperscans (#1603)

* add gzip and brotli support to FetchMode.PROXY mode

* add reaperscans source

* fix chapter ordering for reaperscans

* fix reaperscans duplicated chapter content
This commit is contained in:
Karol
2025-03-31 15:17:12 +02:00
committed by GitHub
parent f11c5835ca
commit 164a1af1b8
2 changed files with 126 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

+126
View File
@@ -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<T> {
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<Plugin.NovelItem[]> {
return this.query(page);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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<string> {
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('<p'))!;
const prefix = content.substring(0, content.indexOf('<'));
const commonPrefix = prefix.substring(
prefix.indexOf(':'),
prefix.indexOf(','),
);
const deduplicated = content.split(commonPrefix)[1];
console.log(prefix, commonPrefix, content.length, deduplicated.length);
return deduplicated.substring(
deduplicated.indexOf('<'),
deduplicated.lastIndexOf('>') + 1,
);
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
return this.query(1, searchTerm);
}
private async query(page = 1, search = ''): Promise<Plugin.NovelItem[]> {
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<ReaperNovel[]> = 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();