Skip to content

Commit ab7c075

Browse files
committed
refactor: ♻️ 重构合并拷贝和热辣通过网页数据获取图片列表的代码
1 parent ee076b3 commit ab7c075

4 files changed

Lines changed: 100 additions & 74 deletions

File tree

rollup.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const packlist = [
6767
'components/Fab',
6868
'components/Toast',
6969
'userscript/dmzjApi',
70+
'userscript/copyApi',
7071
'userscript/detectAd',
7172
'userscript/main',
7273
'worker/detectAd',

src/index.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from 'helper';
2222
import { getInitLang } from 'helper/languages';
2323
import { request, toast, universal } from 'main';
24+
import { getImglistByHtml } from 'userscript/copyApi';
2425
import { otherSite } from 'userscript/otherSite';
2526

2627
/** 站点配置 */
@@ -702,26 +703,16 @@ try {
702703
case 'www.relamanhua.org':
703704
case 'www.manga2024.com':
704705
case 'www.2024manga.com': {
705-
if (
706-
!location.pathname.includes('/chapter/') &&
707-
!document.querySelector('.disData[contentkey]')
708-
)
706+
if (!location.pathname.includes('/chapter/')) break;
707+
708+
if (!document.querySelector('.disData[contentkey]')) {
709+
toast.error(t('site.changed_load_failed'));
709710
break;
710-
const getImgList = async () => {
711-
const [, , comicName, , id] = location.pathname.split('/');
712-
const res = await request<{
713-
results: { chapter: { contents: { url: string }[] } };
714-
}>(
715-
`https://api.manga2025.com/api/v3/comic/${comicName}/chapter/${id}`,
716-
{ responseType: 'json' },
717-
);
718-
return res.response.results.chapter.contents.map(({ url }) =>
719-
url.replace('.h800x.', '.h1500x.'),
720-
);
721-
};
711+
}
712+
722713
options = {
723714
name: 'relamanhua',
724-
getImgList,
715+
getImgList: () => getImglistByHtml(),
725716
onNext: querySelectorClick('.comicContent-next a:not(.prev-null)'),
726717
onPrev: querySelectorClick(
727718
'.comicContent-prev:not(.index,.list) a:not(.prev-null)',

src/site/copymanga.tsx

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
wait,
1414
} from 'helper';
1515
import { request, toast, useInit } from 'main';
16+
import { decryptData, getImglistByHtml } from 'userscript/copyApi';
1617

1718
// API 参考:https://github.com/fumiama/copymanga/blob/279e08b06a70307bf20162900103ec1fdcb97751/app/src/main/res/values/strings.xml
1819

@@ -51,62 +52,6 @@ const pcApi = new (class {
5152
);
5253
})();
5354

54-
// by: https://github.com/MapoMagpie/comic-looms/blob/7799f87fdd5a8ac73c878f338b7ae6aa5c0b2d18/src/platform/matchers/mangacopy.ts#L96-L125
55-
const decryptData = async (
56-
raw: string,
57-
key: string = unsafeWindow.cct || 'oppzzivv.nzm.oip',
58-
) => {
59-
const cipher = raw.slice(16);
60-
const iv = raw.slice(0, 16);
61-
62-
const decryptedBuffer = await crypto.subtle.decrypt(
63-
{ name: 'AES-CBC', iv: new TextEncoder().encode(iv) },
64-
await crypto.subtle.importKey(
65-
'raw',
66-
new TextEncoder().encode(key),
67-
{ name: 'AES-CBC' },
68-
false,
69-
['decrypt'],
70-
),
71-
new Uint8Array(
72-
cipher.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
73-
).buffer,
74-
);
75-
return JSON.parse(new TextDecoder().decode(decryptedBuffer));
76-
};
77-
78-
declare const contentKey: string;
79-
declare const cct: string;
80-
81-
/** 通过解析网页变量获取图片列表 */
82-
const getImglistByHtml = async (comicName: string, id: string) => {
83-
const getKeys = async (): Promise<[string, string]> => {
84-
// 移动端没有 contentKey,就从 PC 端的网页获取
85-
if (!unsafeWindow.contentKey) {
86-
const html = await request(
87-
`${location.origin}/comic/${comicName}/chapter/${id}`,
88-
{
89-
fetch: false,
90-
headers: {
91-
'User-Agent':
92-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.79 Safari/537.36',
93-
},
94-
},
95-
);
96-
const [script] = html.responseText.match(
97-
/(?<=<script>\s+)(var .+?contentKey =.+?)(?=<\/script)/gs,
98-
)!;
99-
eval(script); // oxlint-disable-line no-eval
100-
}
101-
102-
return [contentKey, cct];
103-
};
104-
105-
const keys = await getKeys();
106-
const res: { url: string }[] = await decryptData(...keys);
107-
return res.map(({ url }) => url.replace(/(?<=(\/|\.))c800x/, 'c1500x'));
108-
};
109-
11055
// 在目录页显示上次阅读记录
11156
const handleLastChapter = (comicName: string) => {
11257
let a: HTMLAnchorElement;
@@ -496,7 +441,9 @@ const buildChapters = async (comicName: string, hiddenType: HiddenType) => {
496441
if (titleDom) return getImglistByApi();
497442
// 其他普通漫画优先通过解析网页变量加载,避免触发 api 的限制
498443
try {
499-
const imgList = await getImglistByHtml(comicName, id);
444+
const imgList = await getImglistByHtml(
445+
`${location.origin}/comic/${comicName}/chapter/${id}`,
446+
);
500447
if (imgList.length === 0) throw new Error('解析网页变量失败');
501448
return imgList;
502449
} catch (error) {

src/userscript/copyApi.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { querySelector, t } from 'helper';
2+
import { toast } from 'main';
3+
import { request } from 'request';
4+
5+
let contentKey = '';
6+
let key = '';
7+
8+
const getKeys = async (url?: string): Promise<[string, string]> => {
9+
if (contentKey && key) return [contentKey, key];
10+
11+
// 热辣漫画放在网页元素里
12+
if (querySelector('.disData[contentkey]')) {
13+
contentKey = querySelector('.disData[contentkey]')!.getAttribute(
14+
'contentkey',
15+
)!;
16+
key = querySelector('.disPass[contentkey]')!.getAttribute('contentkey')!;
17+
return [contentKey, key];
18+
}
19+
20+
// 拷贝 PC 端直接放在网页变量里,不过另一个变量的名字会变
21+
if (unsafeWindow.contentKey && unsafeWindow.cct) {
22+
contentKey = unsafeWindow.contentKey; // oxlint-disable-line prefer-destructuring
23+
key = unsafeWindow.cct;
24+
return [contentKey, key];
25+
}
26+
27+
// 如果另一个变量的名字变了,或者是在拷贝的移动端,就得从 PC 端的网页里解析获取了
28+
if (url) {
29+
const html = await request(url, {
30+
fetch: false,
31+
headers: {
32+
'User-Agent':
33+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.79 Safari/537.36',
34+
},
35+
});
36+
const [script] = html.responseText.match(
37+
/(?<=<script>\s+)(var .+?contentKey =.+?)(?=<\/script)/gs,
38+
)!;
39+
40+
const res: Record<string, string> = {};
41+
for (const [, key, value] of script.matchAll(/var (\S+) = '(.+?)';\n/g))
42+
res[key] = value;
43+
44+
contentKey = res.contentKey; // oxlint-disable-line prefer-destructuring
45+
46+
const passKey = Object.keys(res).find((key) => key !== 'contentKey');
47+
if (!passKey) {
48+
toast.error(t('site.changed_load_failed'));
49+
throw new Error(t('site.changed_load_failed'));
50+
}
51+
key = res[passKey];
52+
return [contentKey, key];
53+
}
54+
55+
toast.error(t('site.changed_load_failed'));
56+
throw new Error(t('site.changed_load_failed'));
57+
};
58+
59+
// by: https://github.com/MapoMagpie/comic-looms/blob/7799f87fdd5a8ac73c878f338b7ae6aa5c0b2d18/src/platform/matchers/mangacopy.ts#L96-L125
60+
export const decryptData = async (raw: string, key?: string) => {
61+
key ||= (await getKeys())[1]; // oxlint-disable-line no-await-expression-member
62+
63+
const cipher = raw.slice(16);
64+
const iv = raw.slice(0, 16);
65+
66+
const decryptedBuffer = await crypto.subtle.decrypt(
67+
{ name: 'AES-CBC', iv: new TextEncoder().encode(iv) },
68+
await crypto.subtle.importKey(
69+
'raw',
70+
new TextEncoder().encode(key),
71+
{ name: 'AES-CBC' },
72+
false,
73+
['decrypt'],
74+
),
75+
new Uint8Array(
76+
cipher.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
77+
).buffer,
78+
);
79+
return JSON.parse(new TextDecoder().decode(decryptedBuffer));
80+
};
81+
82+
/** 通过解析网页变量获取图片列表 */
83+
export const getImglistByHtml = async (url?: string) => {
84+
const keys = await getKeys(url);
85+
const res: { url: string }[] = await decryptData(...keys);
86+
return res.map(({ url }) => url.replace(/(?<=(\/|\.))c800x/, 'c1500x'));
87+
};

0 commit comments

Comments
 (0)