Skip to content

Commit 5788cf1

Browse files
committed
fix: 修复由于 jm 解密逻辑变更造成的部分 gif 图片被解密问题
1 parent c385a79 commit 5788cf1

10 files changed

Lines changed: 188 additions & 107 deletions

File tree

electron/module/download.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface DownloadComicItem extends DownloadBaseItem {
1414
comicName: string
1515
chapterName: string
1616
picUrlList: Array<string>
17+
scrambleId: number
18+
speed: string
1719
}
1820

1921
export type DownloadItem = DownloadComicItem

src/apis/ajax.ts

Lines changed: 84 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,59 +1090,96 @@ export const signInApi = (userId: number, dayId: number) => {
10901090

10911091
// 获取图片列表,需要通过正则来解析 html 文件内容
10921092
export const getComicPicListApi = (comicId: number, shuntKey: number | undefined) => {
1093-
return http.Get<{ list: string[] }, string>('chapter_view_template', {
1094-
params: {
1095-
id: comicId,
1096-
mode: 'vertical',
1097-
page: 0,
1098-
app_img_shunt: shuntKey,
1099-
express: 'off',
1100-
v: Math.floor(Date.now() / 1000),
1101-
// id=416130&mode=vertical&page=0&app_img_shunt=1&express=off&v=1727492089
1102-
},
1103-
async transform(htmlStr) {
1104-
// 2025.06.15 新版匹配方式
1105-
// 正则表达式匹配 result 对象
1106-
const resultRegex = /const result\s*=\s*({[\s\S]*?});/
1107-
const resultMatch = htmlStr.match(resultRegex)
1108-
let result: { images: Array<string> } | null = null
1109-
if (resultMatch) {
1110-
try {
1111-
// oxlint-disable-next-line
1112-
result = eval(`(${resultMatch[1]})`)
1113-
} catch (e) {
1114-
console.error('Error parsing result object:', e)
1093+
return http.Get<{ list: string[]; scrambleId: number; speed: string }, string>(
1094+
'chapter_view_template',
1095+
{
1096+
params: {
1097+
id: comicId,
1098+
mode: 'vertical',
1099+
page: 0,
1100+
app_img_shunt: shuntKey,
1101+
express: 'off',
1102+
v: Math.floor(Date.now() / 1000),
1103+
// id=416130&mode=vertical&page=0&app_img_shunt=1&express=off&v=1727492089
1104+
},
1105+
async transform(htmlStr) {
1106+
// 2025.06.15 新版匹配方式
1107+
// 正则表达式匹配 result 对象
1108+
const resultRegex = /const result\s*=\s*({[\s\S]*?});/
1109+
const resultMatch = htmlStr.match(resultRegex)
1110+
let result: { images: Array<string> } | null = null
1111+
if (resultMatch) {
1112+
try {
1113+
// oxlint-disable-next-line
1114+
result = eval(`(${resultMatch[1]})`)
1115+
} catch (e) {
1116+
console.error('Error parsing result object:', e)
1117+
}
11151118
}
1116-
}
11171119

1118-
// 正则表达式匹配 config 对象
1119-
const configRegex = /const config\s*=\s*({[\s\S]*?});/
1120-
const configMatch = htmlStr.match(configRegex)
1121-
let config: {
1122-
cache: string
1123-
imghost: string
1124-
jmid: string
1125-
} | null = null
1126-
if (configMatch) {
1127-
try {
1128-
// oxlint-disable-next-line
1129-
config = eval(`(${configMatch[1]})`)
1130-
} catch (e) {
1131-
console.error('Error parsing config object:', e)
1120+
// 正则表达式匹配 config 对象
1121+
const configRegex = /const config\s*=\s*({[\s\S]*?});/
1122+
const configMatch = htmlStr.match(configRegex)
1123+
let config: {
1124+
cache: string
1125+
imghost: string
1126+
jmid: string
1127+
} | null = null
1128+
if (configMatch) {
1129+
try {
1130+
// oxlint-disable-next-line
1131+
config = eval(`(${configMatch[1]})`)
1132+
} catch (e) {
1133+
console.error('Error parsing config object:', e)
1134+
}
11321135
}
1133-
}
1134-
if (!result || !config) {
1135-
return {
1136-
list: [],
1136+
1137+
// 2026.03.26
1138+
// 新增 scrambleId 和 speed 参数,用于判断是否需要解密
1139+
// 出现 gif 后缀图片
1140+
// 匹配 scrambleId
1141+
const scrambleIdRegex = /var scramble_id\s*=\s*(\d+);/
1142+
const scrambleIdMatch = htmlStr.match(scrambleIdRegex)
1143+
let scrambleId = 0
1144+
if (scrambleIdMatch) {
1145+
try {
1146+
scrambleId = Number.parseInt(scrambleIdMatch[1])
1147+
} catch (e) {
1148+
console.error('Error parsing scrambleId arg:', e)
1149+
}
11371150
}
1138-
}
1139-
return {
1140-
list: result.images.map(
1151+
1152+
// 匹配 speed
1153+
const speedRegex = /var speed\s*=\s*'(.*)';/
1154+
const speedMatch = htmlStr.match(speedRegex)
1155+
let speed = ''
1156+
if (speedMatch) {
1157+
try {
1158+
speed = speedMatch[1]
1159+
} catch (e) {
1160+
console.error('Error parsing speed arg:', e)
1161+
}
1162+
}
1163+
1164+
if (!result || !config) {
1165+
return {
1166+
list: [],
1167+
scrambleId,
1168+
speed,
1169+
}
1170+
}
1171+
const list = result.images.map(
11411172
(item) => `${config.imghost}/media/photos/${config.jmid}/${item}${config.cache}`,
1142-
),
1143-
}
1173+
)
1174+
console.log('list', list)
1175+
return {
1176+
list,
1177+
scrambleId,
1178+
speed,
1179+
}
1180+
},
11441181
},
1145-
})
1182+
)
11461183
// .then(() => {
11471184
// return [
11481185
// "https://cdn-msp.jmapiproxy3.cc/media/photos/113592/00001.webp",

src/components/app-comic-detail-chapter.vue

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const { data, send } = useRequest(
3636
immediate: false,
3737
initialData: {
3838
list: [],
39+
scrambleId: 0,
40+
speed: '',
3941
},
4042
},
4143
)
@@ -45,41 +47,37 @@ const downloadChapter = async (chapter: { id: number; name: string }) => {
4547
snackbar.warning('任务正在下载中,请勿重复点击')
4648
return
4749
}
50+
51+
const exec = async () => {
52+
await send(chapter.id)
53+
downloadStore.addDownloadTaskAction(
54+
{
55+
type: 'comic',
56+
id: chapter.id,
57+
comicName: props.comicName,
58+
chapterName: chapter.name,
59+
picUrlList: data.value.list,
60+
filepath: '',
61+
scrambleId: data.value.scrambleId,
62+
speed: data.value.speed,
63+
},
64+
true,
65+
)
66+
snackbar.success('添加下载任务成功')
67+
info('添加 %s %s 下载任务', props.comicName, chapter.name)
68+
}
4869
if (downloadStore.completeMap[chapter.id]) {
4970
dialog({
5071
width: 300,
5172
title: '确认',
5273
content: '该漫画已下载,是否重新下载?',
5374
async onOk() {
54-
await send(chapter.id)
55-
downloadStore.addDownloadTaskAction(
56-
{
57-
type: 'comic',
58-
id: chapter.id,
59-
comicName: props.comicName,
60-
chapterName: chapter.name,
61-
picUrlList: data.value.list,
62-
filepath: '',
63-
},
64-
true,
65-
)
66-
snackbar.success('添加下载任务成功')
67-
info('添加 %s %s 下载任务', props.comicName, chapter.name)
75+
exec()
6876
},
6977
})
7078
return
7179
}
72-
await send(chapter.id)
73-
downloadStore.addDownloadTaskAction({
74-
type: 'comic',
75-
id: chapter.id,
76-
comicName: props.comicName,
77-
chapterName: chapter.name,
78-
picUrlList: data.value.list,
79-
filepath: '',
80-
})
81-
snackbar.success('添加下载任务成功')
82-
info('添加 %s %s 下载任务', props.comicName, chapter.name)
80+
exec()
8381
}
8482
</script>
8583

src/components/app-comic-page-read.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
const props = defineProps<{
33
comicId: number
44
picList: Array<string>
5+
scrambleId: number
6+
speed: string
57
}>()
68
79
const page = ref(0) // [0, picList.length - 1]
@@ -47,6 +49,8 @@ const onSliderEnd = (value: [number, number] | number) => {
4749
:key="picList[page]"
4850
:comic-id="comicId"
4951
:src="picList[page]"
52+
:scramble-id="scrambleId"
53+
:speed="speed"
5054
@decode-success="onDecodeSuccess(page)"
5155
/>
5256
</div>

src/components/app-comic-scroll-read.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
defineProps<{
33
comicId: number
44
picList: Array<string>
5+
scrambleId: number
6+
speed: string
57
}>()
68
79
const sliderValue = ref(0)
@@ -24,6 +26,8 @@ const onDecodeSuccess = inject<(index: number) => void>('onDecodeSuccess', () =>
2426
:key="item"
2527
:comic-id="comicId"
2628
:src="item"
29+
:scramble-id="scrambleId"
30+
:speed="speed"
2731
@intersect="sliderValue = index + 1"
2832
@decode-success="onDecodeSuccess(index)"
2933
/>

src/components/comic-pic/comic-page-pic.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { decodeImage } from '@/utils/image-decode'
44
const props = defineProps<{
55
src: string
66
comicId: number
7+
scrambleId: number
8+
speed: string
79
}>()
810
const emits = defineEmits<{
911
(e: 'decodeSuccess'): void
@@ -13,7 +15,7 @@ const isLoaded = ref(false)
1315
const imgSrc = ref<string>('')
1416
1517
onMounted(async () => {
16-
imgSrc.value = await decodeImage(props.src, props.comicId)
18+
imgSrc.value = await decodeImage(props.src, props.comicId, props.scrambleId, props.speed)
1719
emits('decodeSuccess')
1820
})
1921
</script>

src/components/comic-pic/comic-vertical-pic.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { decodeImage } from '@/utils/image-decode'
66
const props = defineProps<{
77
src: string
88
comicId: number
9+
scrambleId: number
10+
speed: string
911
}>()
1012
const emits = defineEmits<{
1113
(e: 'intersect'): void
@@ -18,7 +20,7 @@ const imgSrc = ref<string>('')
1820
1921
const onLoadImageIntersect = async (isIntersecting: boolean) => {
2022
if (isIntersecting) {
21-
imgSrc.value = await decodeImage(props.src, props.comicId)
23+
imgSrc.value = await decodeImage(props.src, props.comicId, props.scrambleId, props.speed)
2224
emits('decodeSuccess')
2325
}
2426
}

src/utils/image-decode.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// jm 的从 220980 之后的图都经过混淆
22
// 需要通过 canvas 重绘
33
import { trpcClient } from '@/apis'
4+
import { info } from '@/logger'
45

56
import { getLoadedImage } from '.'
67

7-
export const needDecode = (comicId: number): boolean => {
8-
return comicId > 220980
8+
const isGif = (src: string): boolean => {
9+
return src.endsWith('.gif')
910
}
1011

1112
const seedMap = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
@@ -27,12 +28,36 @@ const getSeed = async (comicId: number, pageStr: string) => {
2728

2829
const decodeSrcMap = new Map<string, string>()
2930
const decodePromiseMap = new Map<string, Promise<string>>()
30-
export const decodeImage = async (src: string, comicId: number) => {
31+
export const decodeImage = async (
32+
src: string,
33+
comicId: number,
34+
scrambleId: number,
35+
speed: string,
36+
) => {
37+
info(
38+
'开始解密地址 %s 图片,所属 comicId 为 %s ,scrambleId 为 %s , speed 为 %s 。',
39+
src,
40+
comicId,
41+
scrambleId,
42+
speed,
43+
)
44+
if (comicId < scrambleId) {
45+
info('comicId 小于 scrambleId ,跳过解密')
46+
}
47+
if (isGif(src)) {
48+
info('gif 格式,跳过解密')
49+
}
50+
if (speed === '1') {
51+
info('speed 参数为 1 ,跳过解密')
52+
}
53+
if (comicId < scrambleId || isGif(src) || speed === '1') {
54+
return src
55+
}
3156
const key = comicId + '-' + src.substring(src.lastIndexOf('/') + 1, src.lastIndexOf('.'))
57+
info('全局解密 promise map key :%s', key)
3258
if (decodeSrcMap.has(key)) {
33-
return decodeSrcMap.get(key)!
34-
}
35-
if (!needDecode(comicId)) {
59+
const src = decodeSrcMap.get(key)!
60+
info('在 map 中存在数据,已解密过,直接返回解密后的 src :%s', src)
3661
return src
3762
}
3863
// 确保只有一个 promise 被加载

0 commit comments

Comments
 (0)