diff --git a/src/components/ComicImage.tsx b/src/components/ComicImage.tsx
index c863a7f..847a295 100644
--- a/src/components/ComicImage.tsx
+++ b/src/components/ComicImage.tsx
@@ -6,15 +6,14 @@ import {
StyleProp,
ImageStyle,
} from 'react-native';
+import { aspectFit, AsyncStatus, LayoutMode, Orientation, ScrambleType, unscramble } from '~/utils';
import { CachedImage, CacheManager } from '@georstat/react-native-image-cache';
-import { aspectFit, AsyncStatus, LayoutMode, Orientation } from '~/utils';
import { useFocusEffect } from '@react-navigation/native';
import { Center, Image } from 'native-base';
import { useDimensions } from '~/hooks';
import Canvas, { Image as CanvasImage } from 'react-native-canvas';
import ErrorWithRetry from '~/components/ErrorWithRetry';
import FastImage, { ImageStyle as FastImageStyle, ResizeMode } from 'react-native-fast-image';
-import md5 from 'blueimp-md5';
const groundPoundGif = require('~/assets/ground_pound.gif');
const windowScale = Dimensions.get('window').scale;
@@ -49,7 +48,8 @@ export interface ImageProps {
onChange?: (state: ImageState, idx?: number) => void;
}
export interface ComicImageProps extends ImageProps {
- useJMC?: boolean;
+ needUnscramble?: boolean;
+ scrambleType?: ScrambleType;
}
const DefaultImage = ({
@@ -192,14 +192,15 @@ const DefaultImage = ({
);
};
-const JMCImage = ({
+const ScrambleImage = ({
uri,
index,
headers = {},
layoutMode = LayoutMode.Horizontal,
prevState = defaultState,
onChange,
-}: ImageProps) => {
+ scrambleType,
+}: ImageProps & { scrambleType?: ScrambleType }) => {
const { width: windowWidth, height: windowHeight, orientation } = useDimensions();
const [imageState, setImageState] = useState(prevState);
const defaultFillHeight = useMemo(() => (windowHeight * 3) / 5, [windowHeight]);
@@ -256,7 +257,7 @@ const JMCImage = ({
// https://github.com/facebook/react-native/issues/33498
const width = event.target.width;
const height = event.target.height;
- const step = unscramble(i, width, height);
+ const step = unscramble(i, width, height, scrambleType);
if (canvasRef.current) {
// if image size more than maxPixelSize, scale image to smaller
@@ -303,7 +304,7 @@ const JMCImage = ({
handleError();
}
},
- [imageState, updateData, handleError, windowWidth, windowHeight]
+ [imageState, updateData, handleError, windowWidth, windowHeight, scrambleType]
);
const loadImage = useCallback(() => {
setImageState((state) => ({ ...state, loadStatus: AsyncStatus.Pending }));
@@ -382,90 +383,9 @@ const styles = StyleSheet.create({
},
});
-function getChapterId(uri: string) {
- const [, id] = uri.match(/\/([0-9]+)\//) || [];
- return Number(id);
-}
-function getPicIndex(uri: string) {
- const [, index] = uri.match(/\/([0-9]+)\./) || [];
- return index;
-}
-function getSplitNum(id: number, index: string) {
- var a = 10;
- if (id >= 268850) {
- const str = md5(id + index);
- const nub = str.substring(str.length - 1).charCodeAt(0) % (id >= 421926 ? 8 : 10);
-
- switch (nub) {
- case 0:
- a = 2;
- break;
- case 1:
- a = 4;
- break;
- case 2:
- a = 6;
- break;
- case 3:
- a = 8;
- break;
- case 4:
- a = 10;
- break;
- case 5:
- a = 12;
- break;
- case 6:
- a = 14;
- break;
- case 7:
- a = 16;
- break;
- case 8:
- a = 18;
- break;
- case 9:
- a = 20;
- }
- }
- return a;
-}
-function unscramble(uri: string, width: number, height: number) {
- const step = [];
- const id = getChapterId(uri);
- const index = getPicIndex(uri);
- const numSplit = getSplitNum(id, index);
- const perheight = height % numSplit;
-
- for (let i = 0; i < numSplit; i++) {
- let sHeight = Math.floor(height / numSplit);
- let dy = sHeight * i;
- const sy = height - sHeight * (i + 1) - perheight;
-
- if (i === 0) {
- sHeight += perheight;
- } else {
- dy += perheight;
- }
-
- step.push({
- sx: 0,
- sy,
- sWidth: width,
- sHeight,
- dx: 0,
- dy,
- dWidth: width,
- dHeight: sHeight,
- });
- }
-
- return step;
-}
-
-const ComicImage = ({ useJMC, ...props }: ComicImageProps) => {
- if (useJMC) {
- return ;
+const ComicImage = ({ scrambleType, needUnscramble, ...props }: ComicImageProps) => {
+ if (needUnscramble) {
+ return ;
}
return ;
diff --git a/src/components/Reader.tsx b/src/components/Reader.tsx
index a774495..c72a734 100644
--- a/src/components/Reader.tsx
+++ b/src/components/Reader.tsx
@@ -8,7 +8,7 @@ import React, {
ForwardRefRenderFunction,
} from 'react';
import { FlashList, ListRenderItemInfo, ViewToken } from '@shopify/flash-list';
-import { LayoutMode, PositionX } from '~/utils';
+import { LayoutMode, PositionX, ScrambleType } from '~/utils';
import { useFocusEffect } from '@react-navigation/native';
import { useDimensions } from '~/hooks';
import { Box, Flex } from 'native-base';
@@ -21,6 +21,7 @@ export interface ReaderProps {
layoutMode?: LayoutMode;
data?: {
uri: string;
+ scrambleType?: ScrambleType;
needUnscramble?: boolean | undefined;
pre: number;
current: number;
@@ -148,7 +149,7 @@ const Reader: ForwardRefRenderFunction = (
onPageChangeRef.current && onPageChangeRef.current(last.item[0].pre + last.item[0].current - 1);
};
const renderHorizontalItem = ({ item, index }: ListRenderItemInfo<(typeof data)[0]>) => {
- const { uri, needUnscramble } = item;
+ const { uri, scrambleType, needUnscramble } = item;
const horizontalState = horizontalStateRef.current[index];
return (
= (
= (
);
};
const renderVerticalItem = ({ item, index }: ListRenderItemInfo<(typeof data)[0]>) => {
- const { uri, needUnscramble } = item;
+ const { uri, scrambleType, needUnscramble } = item;
const verticalState = verticalStateRef.current[index];
return (
@@ -183,7 +185,8 @@ const Reader: ForwardRefRenderFunction = (
= (
alignItems="center"
justifyContent="center"
>
- {item.map(({ uri, needUnscramble, chapterHash, current }, i) => {
+ {item.map(({ uri, scrambleType, needUnscramble, chapterHash, current }, i) => {
const multipleState = (multipleStateRef.current[index] || [])[i];
return (
@@ -218,7 +221,8 @@ const Reader: ForwardRefRenderFunction = (
([
[JMC.id, JMC],
[NH.id, NH],
[PICA.id, PICA],
+ [RM5.id, RM5],
[KL.id, KL],
[BZM.id, BZM],
[DMZJ.id, DMZJ],
diff --git a/src/plugins/jmc.ts b/src/plugins/jmc.ts
index e2d2ef3..81d459e 100644
--- a/src/plugins/jmc.ts
+++ b/src/plugins/jmc.ts
@@ -1,5 +1,5 @@
import Base, { Plugin, Options } from './base';
-import { MangaStatus, ErrorMessage } from '~/utils';
+import { MangaStatus, ErrorMessage, ScrambleType } from '~/utils';
import { Platform } from 'react-native';
import * as cheerio from 'cheerio';
@@ -354,6 +354,7 @@ class CopyManga extends Base {
},
images: images.map((uri) => ({
uri,
+ type: ScrambleType.JMC,
needUnscramble: !uri.includes('.gif') && Number(chapterId) >= Number(scrambleId),
})),
},
diff --git a/src/plugins/rm5.ts b/src/plugins/rm5.ts
new file mode 100644
index 0000000..7dfc3b3
--- /dev/null
+++ b/src/plugins/rm5.ts
@@ -0,0 +1,319 @@
+import Base, { Plugin, Options } from './base';
+import { MangaStatus, ErrorMessage, ScrambleType } from '~/utils';
+import * as cheerio from 'cheerio';
+import moment from 'moment';
+
+interface ScriptData {
+ props: {
+ pageProps: T;
+ __N_SSP: boolean;
+ };
+ page: string;
+ query: Record;
+ buildId: string;
+ isFallback: boolean;
+ gssp: boolean;
+ scriptLoader: string[];
+}
+
+interface DiscoverySearchData
+ extends ScriptData<{
+ books: {
+ id: string;
+ name: string;
+ alias: string[];
+ description: string;
+ coverUrl: string;
+ author: string;
+ continued: boolean;
+ tags: string[];
+ rating: number | null;
+ publish: boolean;
+ /**
+ * @example 2023-10-19T00:00:00.000Z
+ */
+ updatedAt: string;
+ coverUrlRectangle: string;
+ coverUrlSquare: string;
+ }[];
+ tags: { id: string; count: number }[];
+ hasNextPage: boolean;
+ }> {}
+
+interface MangaData
+ extends ScriptData<{
+ book: {
+ id: string;
+ name: string;
+ description: string;
+ alias: string[];
+ tags: string[];
+ author: string;
+ coverUrl: string;
+ coverUrlRectangle: string;
+ coverUrlSquare: string;
+ rating: number | null;
+ continued: boolean;
+ viewCount: number;
+ publish: boolean;
+ /**
+ * @example 2023-10-19T00:00:00.000Z
+ */
+ createdAt: string;
+ /**
+ * @example 2023-10-19T00:00:00.000Z
+ */
+ updatedAt: string;
+ activeResourceId: string;
+ activeResource: {
+ id: string;
+ description: string;
+ coverUrl: string;
+ author: string;
+ continued: boolean;
+ tags: string[];
+ chapters: string[];
+ resourceKey: string;
+ resourceRef: string;
+ folderPath: string;
+ /**
+ * @example 2023-10-19T00:00:00.000Z
+ */
+ createdAt: string;
+ /**
+ * @example 2023-10-19T00:00:00.000Z
+ */
+ updatedAt: string;
+ bookId: string;
+ };
+ };
+ onMyShelf: boolean;
+ lastReadChapterIndex: number;
+ session: string | null;
+ adBookBottom: boolean;
+ siteDomain: string;
+ }> {}
+
+interface ChapterData
+ extends ScriptData<{
+ bookName: string;
+ alias: string[];
+ chapterName: string;
+ description: string;
+ images: { src: string; scramble: boolean }[];
+ totalChapter: number;
+ tags: string[];
+ session: string | null;
+ adBookBottom: boolean;
+ }> {}
+
+const discoveryOptions = [
+ {
+ name: 'type',
+ options: [
+ { label: '選擇分類', value: Options.Default },
+ { label: '正妹', value: '正妹' },
+ { label: '恋爱', value: '恋爱' },
+ { label: '出版漫画', value: '出版漫画' },
+ { label: '肉慾', value: '肉慾' },
+ { label: '浪漫', value: '浪漫' },
+ { label: '大尺度', value: '大尺度' },
+ { label: '巨乳', value: '巨乳' },
+ { label: '有夫之婦', value: '有夫之婦' },
+ { label: '女大生', value: '女大生' },
+ { label: '狗血劇', value: '狗血劇' },
+ { label: '好友', value: '好友' },
+ { label: '調教', value: '調教' },
+ { label: '动作', value: '动作' },
+ { label: '後宮', value: '後宮' },
+ { label: '不倫', value: '不倫' },
+ ],
+ },
+ {
+ name: 'status',
+ options: [
+ { label: '選擇狀態', value: Options.Default },
+ { label: '連載中', value: 'true' },
+ { label: '已完結', value: 'false' },
+ ],
+ },
+ {
+ name: 'sort',
+ options: [
+ { label: '選擇排序', value: Options.Default },
+ { label: '更新日期', value: Options.Default },
+ { label: '評分', value: 'rating' },
+ ],
+ },
+];
+
+class RouMan5 extends Base {
+ constructor() {
+ const userAgent =
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36';
+ super({
+ score: 5,
+ id: Plugin.RM5,
+ name: '肉满屋',
+ shortName: 'RM5',
+ description: '需要代理,都是韩漫',
+ href: 'https://rouman5.com/',
+ userAgent,
+ defaultHeaders: { Referer: 'https://rouman5.com/', 'User-Agent': userAgent },
+ option: { discovery: discoveryOptions, search: [] },
+ });
+ }
+
+ prepareDiscoveryFetch: Base['prepareDiscoveryFetch'] = (page, { type, status, sort }) => {
+ return {
+ url: 'https://rouman5.com/books',
+ body: {
+ tag: type === Options.Default ? undefined : type,
+ continued: status === Options.Default ? undefined : status,
+ sort: sort === Options.Default ? undefined : sort,
+ page,
+ },
+ headers: new Headers(this.defaultHeaders),
+ };
+ };
+ prepareSearchFetch: Base['prepareSearchFetch'] = (keyword, page) => {
+ return {
+ url: 'https://rouman5.com/search',
+ body: {
+ term: keyword,
+ page,
+ },
+ headers: new Headers(this.defaultHeaders),
+ };
+ };
+ prepareMangaInfoFetch: Base['prepareMangaInfoFetch'] = (mangaId) => {
+ return {
+ url: `https://rouman5.com/books/${mangaId}`,
+ headers: new Headers(this.defaultHeaders),
+ };
+ };
+ prepareChapterListFetch: Base['prepareChapterListFetch'] = () => {};
+ prepareChapterFetch: Base['prepareChapterFetch'] = (mangaId, chapterId) => {
+ return {
+ url: `https://rouman5.com/books/${mangaId}/${chapterId}`,
+ headers: new Headers(this.defaultHeaders),
+ };
+ };
+
+ handleDiscovery: Base['handleDiscovery'] = (text: string | null) => {
+ const $ = cheerio.load(text || '');
+ const scriptLabel =
+ ($('script[id=__NEXT_DATA__]')[0] as cheerio.TagElement).children[0].data || '';
+ const data: DiscoverySearchData = JSON.parse(scriptLabel);
+ return {
+ discovery: data.props.pageProps.books.map((item) => ({
+ href: `https://rouman5.com/books/${item.id}`,
+ hash: Base.combineHash(this.id, item.id),
+ source: this.id,
+ sourceName: this.name,
+ mangaId: item.id,
+ bookCover: item.coverUrl,
+ title: item.name,
+ updateTime: moment(item.updatedAt).format('YYYY-MM-DD'),
+ headers: this.defaultHeaders,
+ status: item.continued ? MangaStatus.Serial : MangaStatus.End,
+ author: [item.author],
+ tag: item.tags,
+ })),
+ };
+ };
+
+ handleSearch: Base['handleSearch'] = (text: string | null) => {
+ const $ = cheerio.load(text || '');
+ const scriptLabel =
+ ($('script[id=__NEXT_DATA__]')[0] as cheerio.TagElement).children[0].data || '';
+ const data: DiscoverySearchData = JSON.parse(scriptLabel);
+ return {
+ search: data.props.pageProps.books.map((item) => ({
+ href: `https://rouman5.com/books/${item.id}`,
+ hash: Base.combineHash(this.id, item.id),
+ source: this.id,
+ sourceName: this.name,
+ mangaId: item.id,
+ bookCover: item.coverUrl,
+ title: item.name,
+ updateTime: moment(item.updatedAt).format('YYYY-MM-DD'),
+ headers: this.defaultHeaders,
+ status: item.continued ? MangaStatus.Serial : MangaStatus.End,
+ author: [item.author],
+ tag: item.tags,
+ })),
+ };
+ };
+
+ handleMangaInfo: Base['handleMangaInfo'] = (text: string | null) => {
+ const $ = cheerio.load(text || '');
+ const scriptLabel =
+ ($('script[id=__NEXT_DATA__]')[0] as cheerio.TagElement).children[0].data || '';
+ const data: MangaData = JSON.parse(scriptLabel);
+ const { id, name, tags, author, continued, updatedAt, activeResource } =
+ data.props.pageProps.book;
+
+ return {
+ manga: {
+ href: `https://rouman5.com/books/${id}`,
+ hash: Base.combineHash(this.id, id),
+ source: this.id,
+ sourceName: this.name,
+ mangaId: id,
+ title: name,
+ latest:
+ activeResource.chapters.length > 0
+ ? activeResource.chapters[activeResource.chapters.length - 1]
+ : undefined,
+ updateTime: moment(updatedAt).format('YYYY-MM-DD'),
+ author: [author],
+ tag: tags,
+ status: continued ? MangaStatus.Serial : MangaStatus.End,
+ chapters: activeResource.chapters
+ .map((title, index) => ({
+ hash: Base.combineHash(this.id, id, String(index)),
+ mangaId: id,
+ chapterId: String(index),
+ href: `https://rouman5.com/books/${id}/${index}`,
+ title,
+ }))
+ .reverse(),
+ },
+ };
+ };
+
+ handleChapterList: Base['handleChapterList'] = () => {
+ return { error: new Error(ErrorMessage.NoSupport + 'handleChapterList') };
+ };
+ handleChapter: Base['handleChapter'] = (
+ text: string | null,
+ mangaId: string,
+ chapterId: string
+ ) => {
+ const $ = cheerio.load(text || '');
+ const scriptLabel =
+ ($('script[id=__NEXT_DATA__]')[0] as cheerio.TagElement).children[0].data || '';
+ const data: ChapterData = JSON.parse(scriptLabel);
+ const { bookName, chapterName, images } = data.props.pageProps;
+
+ return {
+ canLoadMore: false,
+ chapter: {
+ hash: Base.combineHash(this.id, mangaId, chapterId),
+ mangaId,
+ chapterId,
+ name: bookName,
+ title: chapterName,
+ headers: this.defaultHeaders,
+ images: images.map((item) => ({
+ uri: item.src,
+ scrambleType: ScrambleType.RM5,
+ needUnscramble: !item.src.includes('.gif') && item.scramble,
+ })),
+ },
+ };
+ };
+}
+
+export default new RouMan5();
diff --git a/src/types/schema.json b/src/types/schema.json
index fded09a..a31400e 100644
--- a/src/types/schema.json
+++ b/src/types/schema.json
@@ -144,7 +144,8 @@
"MHGM",
"MHM",
"NH",
- "PICA"
+ "PICA",
+ "RM5"
],
"type": "string"
},
diff --git a/src/types/store.d.ts b/src/types/store.d.ts
index 594fd8b..7c37ddd 100644
--- a/src/types/store.d.ts
+++ b/src/types/store.d.ts
@@ -51,7 +51,7 @@ declare global {
/** 章节名 */
title: string;
headers?: Record;
- images: { uri: string; needUnscramble?: boolean }[];
+ images: { uri: string; needUnscramble?: boolean; scrambleType?: ScrambleType }[];
}
interface Release {
loadStatus: AsyncStatus;
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 974c7b7..dc48883 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -1,10 +1,13 @@
import { Draft, Draft07, JsonError, JsonSchema } from 'json-schema-library';
+import { ErrorMessage, ScrambleType } from './enum';
import { delay, race, Effect } from 'redux-saga/effects';
-import { ErrorMessage } from './enum';
import { Platform } from 'react-native';
+import { Buffer } from 'buffer';
import CookieManager from '@react-native-cookies/cookies';
import queryString from 'query-string';
import CryptoJS from 'crypto-js';
+import base64 from 'base-64';
+import md5 from 'blueimp-md5';
export const PATTERN_VERSION = /v?([0-9]+)\.([0-9]+)\.([0-9]+)/;
export const PATTERN_PUBLISH_TIME = /([0-9]+)-([0-9]+)-([0-9]+)/;
@@ -233,3 +236,132 @@ export function pairsToDict(list: KeyValuePair[]) {
return dict;
}, {});
}
+
+function getChapterId(uri: string) {
+ const [, id] = uri.match(/\/([0-9]+)\//) || [];
+ return Number(id);
+}
+function getPicIndex(uri: string) {
+ const [, index] = uri.match(/\/([0-9]+)\./) || [];
+ return index;
+}
+function getSplitNum(id: number, index: string) {
+ var a = 10;
+ if (id >= 268850) {
+ const str = md5(id + index);
+ const nub = str.substring(str.length - 1).charCodeAt(0) % (id >= 421926 ? 8 : 10);
+
+ switch (nub) {
+ case 0:
+ a = 2;
+ break;
+ case 1:
+ a = 4;
+ break;
+ case 2:
+ a = 6;
+ break;
+ case 3:
+ a = 8;
+ break;
+ case 4:
+ a = 10;
+ break;
+ case 5:
+ a = 12;
+ break;
+ case 6:
+ a = 14;
+ break;
+ case 7:
+ a = 16;
+ break;
+ case 8:
+ a = 18;
+ break;
+ case 9:
+ a = 20;
+ }
+ }
+ return a;
+}
+export function unscrambleJMC(uri: string, width: number, height: number) {
+ const step = [];
+ const id = getChapterId(uri);
+ const index = getPicIndex(uri);
+ const numSplit = getSplitNum(id, index);
+ const perheight = height % numSplit;
+
+ for (let i = 0; i < numSplit; i++) {
+ let sHeight = Math.floor(height / numSplit);
+ let dy = sHeight * i;
+ const sy = height - sHeight * (i + 1) - perheight;
+
+ if (i === 0) {
+ sHeight += perheight;
+ } else {
+ dy += perheight;
+ }
+
+ step.push({
+ sx: 0,
+ sy,
+ sWidth: width,
+ sHeight,
+ dx: 0,
+ dy,
+ dWidth: width,
+ dHeight: sHeight,
+ });
+ }
+
+ return step;
+}
+
+export function unscrambleRM5(uri: string, width: number, height: number) {
+ const step = [];
+ const list = uri.split('/');
+ const id = list[list.length - 1].replace('.jpg', '');
+
+ const buffer = Buffer.from(CryptoJS.MD5(base64.decode(id)).toString(), 'hex');
+ const nub = buffer[buffer.length - 1];
+ const numSplit = (nub % 10) + 5;
+ const perheight = height % numSplit;
+
+ for (let i = 0; i < numSplit; i++) {
+ let sHeight = Math.floor(height / numSplit);
+ let dy = sHeight * i;
+ const sy = height - sHeight * (i + 1) - perheight;
+
+ if (i === 0) {
+ sHeight += perheight;
+ } else {
+ dy += perheight;
+ }
+
+ step.push({
+ sx: 0,
+ sy,
+ sWidth: width,
+ sHeight,
+ dx: 0,
+ dy,
+ dWidth: width,
+ dHeight: sHeight,
+ });
+ }
+
+ return step;
+}
+
+export function unscramble(uri: string, width: number, height: number, type = ScrambleType.JMC) {
+ switch (type) {
+ case ScrambleType.RM5: {
+ return unscrambleRM5(uri, width, height);
+ }
+ case ScrambleType.JMC:
+ default: {
+ return unscrambleJMC(uri, width, height);
+ }
+ }
+}
diff --git a/src/utils/enum.ts b/src/utils/enum.ts
index 74baeb4..18c7f9b 100644
--- a/src/utils/enum.ts
+++ b/src/utils/enum.ts
@@ -112,3 +112,8 @@ export enum PositionXY {
BottomMid,
BottomRight,
}
+
+export enum ScrambleType {
+ JMC,
+ RM5,
+}
diff --git a/src/views/Chapter.tsx b/src/views/Chapter.tsx
index 6156284..11af842 100644
--- a/src/views/Chapter.tsx
+++ b/src/views/Chapter.tsx
@@ -7,6 +7,7 @@ import {
ReaderDirection,
PositionX,
Orientation,
+ ScrambleType,
} from '~/utils';
import { Box, Text, Flex, Center, StatusBar, useToast, useDisclose } from 'native-base';
import { useOnce, usePrevNext, useVolumeUpDown, useDimensions } from '~/hooks';
@@ -42,6 +43,7 @@ const useChapterFlat = (hashList: string[], dict: RootState['dict']['chapter'])
return useMemo(() => {
const list: {
uri: string;
+ scrambleType?: ScrambleType;
needUnscramble?: boolean | undefined;
pre: number;
multiplePre: number;