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:
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -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();
|
||||||
Reference in New Issue
Block a user