Skip to content

Commit c56b09f

Browse files
committed
fix: 🐛 修复 ehentai 广告识别失效的 bug
1 parent 5116129 commit c56b09f

11 files changed

Lines changed: 120 additions & 58 deletions

File tree

src/components/Manga/actions/imageLoad.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import {
77
singleThreaded,
88
t,
99
} from 'helper';
10-
import { request } from 'request';
10+
import { downloadImgHeaders as headers, request } from 'request';
1111

12-
import { downloadImgHeaders as headers } from '../helper';
1312
import { setState, store } from '../store';
1413
import { getImg, getImgEle, getImgIndexs } from './helper';
1514
import { handleImgRecognition } from './imageRecognition';

src/components/Manga/helper.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { RequestDetails } from 'request';
22

3-
import { t } from 'helper';
4-
import { request } from 'request';
3+
import { downloadImg as _downloadImg } from 'request';
54

65
import { setState, store } from './store';
76

@@ -19,11 +18,6 @@ export const playAnimation = (e?: HTMLElement) => {
1918
}
2019
};
2120

22-
export const downloadImgHeaders = {
23-
Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
24-
'User-Agent': navigator.userAgent,
25-
Referer: location.href,
26-
};
2721
export const downloadImg = async (
2822
imgUrl: string,
2923
details?: RequestDetails<Blob>,
@@ -35,16 +29,8 @@ export const downloadImg = async (
3529
return res.blob();
3630
}
3731

38-
const res = await request<Blob>(url, {
39-
responseType: 'blob',
40-
errorText: t('translation.tip.download_img_failed'),
41-
headers: downloadImgHeaders,
42-
retryFetch: true,
43-
...details,
44-
});
45-
32+
const res = await _downloadImg(url, details);
4633
if (Reflect.has(store.imgMap, imgUrl))
47-
setState('imgMap', imgUrl, 'blobUrl', URL.createObjectURL(res.response));
48-
49-
return res.response;
34+
setState('imgMap', imgUrl, 'blobUrl', URL.createObjectURL(res));
35+
return res;
5036
};

src/components/Manga/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,5 @@ export { SettingsItem } from './components/SettingsItem';
104104
export { SettingsItemButton } from './components/SettingsItemButton';
105105
export { SettingsItemNumber } from './components/SettingsItemNumber';
106106
export { SettingsItemSwitch } from './components/SettingsItemSwitch';
107-
export { downloadImg, downloadImgHeaders } from './helper';
108107
export type { ComicImg } from './store/image';
109108
export { refs, setState, store } from './store/index';

src/helper/spriteImage.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { downloadImg } from '../request';
2+
3+
export const imageBitmapCache = new Map<string, ImageBitmap>();
4+
5+
/** 下载图片并转换为ImageBitmap */
6+
const loadImageBitmap = async (url: string): Promise<ImageBitmap> => {
7+
// 如果缓存中已有,直接返回
8+
if (imageBitmapCache.has(url)) return imageBitmapCache.get(url)!;
9+
10+
const blob = await downloadImg(url);
11+
const imageBitmap = await createImageBitmap(blob);
12+
imageBitmapCache.set(url, imageBitmap);
13+
return imageBitmap;
14+
};
15+
16+
/** 从雪碧图中切割指定区域的图片 */
17+
export const extractSpriteImage = async (style: CSSStyleDeclaration) => {
18+
const {
19+
width,
20+
height,
21+
backgroundImage,
22+
backgroundPositionX: backgroundX,
23+
backgroundPositionY: backgroundY,
24+
} = style;
25+
26+
const urlMatch = backgroundImage.match(/url\(['"]([^)]+)['"]\)/);
27+
if (!urlMatch) throw new Error('解析不到背景图片URL');
28+
const [, url] = urlMatch;
29+
30+
const spriteImage = await loadImageBitmap(url);
31+
32+
const w = parseFloat(width);
33+
const h = parseFloat(height);
34+
const canvas = new OffscreenCanvas(w, h);
35+
const ctx = canvas.getContext('2d')!;
36+
37+
ctx.clearRect(0, 0, w, h);
38+
39+
const sourceX = -parseFloat(backgroundX);
40+
const sourceY = -parseFloat(backgroundY);
41+
42+
ctx.drawImage(spriteImage, sourceX, sourceY, w, h, 0, 0, w, h);
43+
44+
return canvas.transferToImageBitmap();
45+
};

src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { InitOptions } from 'main';
22

3-
import { downloadImgHeaders, type MangaProps } from 'components/Manga';
3+
import { type MangaProps } from 'components/Manga';
44
import {
55
fileType,
66
isUrl,
@@ -23,8 +23,7 @@ import { request, toast, universal } from 'main';
2323
import { getImglistByHtml } from 'userscript/copyApi';
2424
import { otherSite } from 'userscript/otherSite';
2525

26-
import type { RequestDetails } from './request';
27-
26+
import { downloadImgHeaders, type RequestDetails } from './request';
2827
import { getNhentaiData, toImgList } from './userscript/nhentaiApi';
2928

3029
/** 站点配置 */

src/request.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,28 @@ export const eachApi = async <T = any>(
170170
log.error('所有 api 请求均失败', url, baseUrlList, details);
171171
throw new Error(errorText);
172172
};
173+
174+
export const downloadImgHeaders = {
175+
Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
176+
'User-Agent': navigator.userAgent,
177+
Referer: location.href,
178+
};
179+
export const downloadImg = async (
180+
url: string,
181+
details?: RequestDetails<Blob>,
182+
) => {
183+
if (url.startsWith('blob:')) {
184+
const res = await fetch(url);
185+
return res.blob();
186+
}
187+
188+
const res = await request<Blob>(url, {
189+
responseType: 'blob',
190+
errorText: t('translation.tip.download_img_failed'),
191+
headers: downloadImgHeaders,
192+
retryFetch: true,
193+
...details,
194+
});
195+
196+
return res.response;
197+
};

src/site/ehentai/detectAd.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { getAdPageByContent, getAdPageByFileName } from 'userscript/detectAd';
44

55
import type { GalleryContext } from './helper';
66

7+
import { extractSpriteImage } from '../../helper/spriteImage';
8+
79
/** 识别广告 */
810
export const detectAd = ({
911
store: { comicMap },
@@ -20,43 +22,43 @@ export const detectAd = ({
2022
setState('comicMap', '', 'adList', new ReactiveSet());
2123

2224
/** 缩略图列表 */
23-
const thumbnailList: (string | HTMLImageElement)[] = [];
24-
for (const e of querySelectorAll<HTMLAnchorElement>('#gdt > a')) {
25-
const index = Number(/.+-(\d+)/.exec(e.href)?.[1]) - 1;
26-
if (Number.isNaN(index)) continue;
27-
pageList[index] = e.href;
25+
const thumbnailList: (ImageBitmap | HTMLImageElement)[] = [];
26+
(async () => {
27+
for (const e of querySelectorAll<HTMLAnchorElement>('#gdt > a')) {
28+
const index = Number(/.+-(\d+)/.exec(e.href)?.[1]) - 1;
29+
if (Number.isNaN(index)) continue;
30+
pageList[index] = e.href;
2831

29-
const thumbnail = e.querySelector<HTMLElement>('[title]')!;
30-
[, fileNameList[index]] = thumbnail.title.split(/|: /);
31-
thumbnailList[index] =
32-
thumbnail.tagName === 'IMG'
33-
? (thumbnail as HTMLImageElement)
34-
: /url\("(.+)"\)/.exec(thumbnail.style.backgroundImage)![1];
35-
}
32+
const thumbnail = e.querySelector<HTMLElement>('[title]')!;
33+
[, fileNameList[index]] = thumbnail.title.split(/|: /);
34+
if (thumbnail.tagName === 'IMG')
35+
thumbnailList[index] = thumbnail as HTMLImageElement;
36+
if (thumbnail.style.background.includes('url('))
37+
thumbnailList[index] = await extractSpriteImage(thumbnail.style);
38+
}
3639

37-
(async () => {
3840
// 先根据文件名判断一次
3941
await getAdPageByFileName(fileNameList, comicMap[''].adList!);
4042
// 不行的话再用缩略图识别
4143
if (comicMap[''].adList!.size === 0)
4244
await getAdPageByContent(thumbnailList, comicMap[''].adList!);
45+
})();
4346

44-
// 模糊广告页的缩略图
45-
useStyle(
46-
createRootMemo(() => {
47-
if (!comicMap['']?.adList?.size) return '';
48-
return [...comicMap[''].adList]
49-
.map(
50-
(i) => `a[href="${pageList[i]}"] [title]:not(:hover) {
47+
// 模糊广告页的缩略图
48+
useStyle(
49+
createRootMemo(() => {
50+
if (!comicMap['']?.adList?.size) return '';
51+
return [...comicMap[''].adList]
52+
.map(
53+
(i) => `a[href="${pageList[i]}"] [title]:not(:hover) {
5154
filter: blur(8px);
5255
clip-path: border-box;
5356
backdrop-filter: blur(8px);
5457
}`,
55-
)
56-
.join('\n');
57-
}),
58-
);
59-
})();
58+
)
59+
.join('\n');
60+
}),
61+
);
6062

6163
// 返回在图片加载时检查图片的函数
6264
return {

src/site/ehentai/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,11 @@ import { tagLint } from './tagLint';
311311
return { src: context.imgList[i], name: context.fileNameList[i] };
312312
},
313313
length: () => loadImgs().length,
314-
onEnd: checkAd?.checkContent,
314+
// 在最后十页的图片url加载出来后再检查广告
315+
onLoad:
316+
checkAd?.checkContent &&
317+
((_, __, list) =>
318+
list.slice(-10, -1).every(Boolean) && checkAd?.checkContent()),
315319
});
316320
},
317321
});

src/userscript/detectAd.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,13 @@ export const isAdImg = (imgBitmap: ImageBitmap) => {
8989

9090
/** 通过图片内容判断是否是广告 */
9191
export const getAdPageByContent = (
92-
imgList: (HTMLImageElement | string | undefined)[],
92+
imgList: (HTMLImageElement | string | ImageBitmap | undefined)[],
9393
adList: Set<number>,
9494
) =>
9595
getAdPage(
9696
imgList,
97-
async (img: HTMLImageElement | string) => isAdImg(await imgToCanvas(img)),
97+
async (img: HTMLImageElement | string | ImageBitmap) =>
98+
isAdImg(img instanceof ImageBitmap ? img : await imgToCanvas(img)),
9899
adList,
99100
);
100101

src/userscript/main/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ export type MainContext<T extends Record<string, any> = Record<string, any>> = {
8888
id?: string | number;
8989
/** 并发数 */
9090
concurrency?: number;
91-
/** 加载完成后触发的回调 */
92-
onEnd?: () => void;
91+
/** 加载完成一个后触发的回调 */
92+
onLoad?: (
93+
img: string | ComicImgData,
94+
index: number,
95+
imgList: (string | ComicImgData)[],
96+
) => void;
9397
}) => Promise<MangaProps['imgList']>;
9498
};

0 commit comments

Comments
 (0)