From 0202cc77fc6cb1475926e0b9b52436f2a79c45b7 Mon Sep 17 00:00:00 2001 From: youniaogu Date: Sat, 21 Oct 2023 12:00:08 +0800 Subject: [PATCH] rouman5 plugin --- src/components/ComicImage.tsx | 102 ++--------- src/components/Reader.tsx | 18 +- src/plugins/base.ts | 1 + src/plugins/index.ts | 2 + src/plugins/jmc.ts | 3 +- src/plugins/rm5.ts | 319 ++++++++++++++++++++++++++++++++++ src/types/schema.json | 3 +- src/types/store.d.ts | 2 +- src/utils/common.ts | 134 +++++++++++++- src/utils/enum.ts | 5 + src/views/Chapter.tsx | 2 + 11 files changed, 489 insertions(+), 102 deletions(-) create mode 100644 src/plugins/rm5.ts 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;