Skip to content

Commit 3a63511

Browse files
authored
Merge pull request #329 from dyphire/nhentai
feat: nhentai API 更新 v2 接口,修复使用
2 parents cd05165 + 2a07c40 commit 3a63511

3 files changed

Lines changed: 224 additions & 131 deletions

File tree

src/site/ehentai/crossSiteLink.tsx

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { For, Show } from 'solid-js';
44
import { createStore } from 'solid-js/store';
55
import { render } from 'solid-js/web';
66

7-
import { fileType, hijackFn, querySelector, querySelectorAll, t } from 'helper';
7+
import { hijackFn, querySelector, querySelectorAll, t } from 'helper';
88
import { request, toast } from 'main';
99

10-
import { searchNhentai } from '../../userscript/nhentaiApi';
10+
import { searchNhentai, getNhentaiData, getNhentaiImageUrl } from '../../userscript/nhentaiApi';
1111
import { type GalleryContext, isInCategories } from './helper';
1212

1313
type ItemData = {
@@ -24,34 +24,36 @@ type SiteFn = {
2424
};
2525

2626
const nhentai: SiteFn = async ({ setState, galleryTitle }) => {
27-
const downImg = async (i: number, media_id: string, type: string) => {
28-
const imgRes = await request<Blob>(
29-
`https://i.nhentai.net/galleries/${media_id}/${i + 1}.${fileType[type]}`,
30-
{
31-
headers: { Referer: `https://nhentai.net/g/${media_id}` },
32-
responseType: 'blob',
33-
fetch: false,
34-
},
35-
);
36-
return URL.createObjectURL(imgRes.response);
37-
};
38-
3927
const result = await searchNhentai(galleryTitle!);
40-
return result.map(({ id, title, images, num_pages, media_id }) => {
28+
return result.map(({ id, media_id, english_title, japanese_title }) => {
4129
const itemId = `@nh:${id}`;
30+
31+
const galleryPromise = getNhentaiData(String(id));
32+
4233
setState('comicMap', itemId, {
43-
getImgList: ({ dynamicLazyLoad }) =>
44-
dynamicLazyLoad({
45-
loadImg: (i) => downImg(i, media_id, images.pages[i].t),
46-
length: num_pages,
34+
getImgList: async ({ dynamicLazyLoad }) => {
35+
const gallery = await galleryPromise;
36+
return dynamicLazyLoad({
37+
loadImg: async (i) => {
38+
const url = getNhentaiImageUrl(gallery, i);
39+
if (!url) throw new Error('nhentai image url not found');
40+
const imgRes = await request<Blob>(url, {
41+
headers: { Referer: `https://nhentai.net/g/${id}` },
42+
responseType: 'blob',
43+
fetch: false,
44+
});
45+
return URL.createObjectURL(imgRes.response);
46+
},
47+
length: gallery.num_pages,
4748
id: itemId,
48-
}),
49+
});
50+
},
4951
});
5052

5153
return {
5254
id: itemId,
5355
showText: `${id}`,
54-
title: title.english || title.japanese,
56+
title: japanese_title || english_title || '',
5557
href: `https://nhentai.net/g/${id}`,
5658
class: 'gtl',
5759
};

src/site/nhentai.tsx

Lines changed: 148 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import {
33
domParse,
44
fileType,
55
log,
6+
onUrlChange,
67
querySelector,
78
querySelectorAll,
89
scrollIntoView,
910
singleThreaded,
1011
t,
1112
useStyle,
13+
wait,
1214
} from 'helper';
1315
import { ReactiveSet, request, toast, useInit } from 'main';
1416
import { getAdPageByContent } from 'userscript/detectAd';
17+
import { getNhentaiData, getNhentaiImageUrl } from '../userscript/nhentaiApi';
1518

1619
type Images = {
1720
thumbnail: { t: keyof typeof fileType; w: number; h: number };
@@ -31,47 +34,76 @@ declare const _gallery: { num_pages: number; media_id: string; images: Images };
3134
detect_ad: true,
3235
});
3336

34-
// 在漫画详情页
35-
if (Reflect.has(unsafeWindow, 'gallery')) {
37+
let currentGalleryId: string | null = null;
38+
39+
const removeComicReadMode = () => {
40+
querySelector('#comicReadMode')?.remove();
41+
};
42+
43+
const createComicReadMode = () => {
44+
const el = document.createElement('a');
45+
el.href = 'javascript:;';
46+
el.id = 'comicReadMode';
47+
el.className = 'btn btn-secondary';
48+
el.addEventListener('click', showComic);
49+
el.innerHTML = '<i class="fa fa-book"></i> Read';
50+
return el;
51+
};
52+
53+
const setupDetailPage = async (id: string) => {
54+
if (currentGalleryId === id) return;
55+
currentGalleryId = id;
56+
57+
removeComicReadMode();
58+
3659
setState('manga', {
3760
onExit(isEnd) {
3861
if (isEnd) scrollIntoView('#comment-container');
3962
setState('manga', 'show', false);
4063
},
4164
});
4265

43-
// nh 自己是每张图随机选一个 cdn,但反正只是分流,简单点顺序分配应该也没问题吧
44-
const cdn = unsafeWindow._n_app.options.image_cdn_urls as string[];
45-
const getImgList = () =>
46-
_gallery.images.pages.map(({ t, w: width, h: height }, i) => {
47-
const src = `https://${cdn[i % cdn.length]}/galleries/${_gallery.media_id}/${i + 1}.${fileType[t]}`;
48-
return { src, width, height };
49-
});
50-
setState('comicMap', '', { getImgList });
66+
let gallery: any = null;
67+
try {
68+
gallery = await getNhentaiData(id);
69+
} catch (error) {
70+
log.error('nhentai getNhentaiData failed', error);
71+
}
72+
73+
if (gallery) {
74+
const getImgList = () =>
75+
gallery.pages.map((page: any, i: number) => ({
76+
src: getNhentaiImageUrl(gallery, i),
77+
width: page.width,
78+
height: page.height,
79+
}));
80+
81+
setState('comicMap', '', { getImgList });
82+
}
83+
5184
setState('fab', 'initialShow', options.autoShow);
5285

53-
const comicReadModeDom = (
54-
<a
55-
href="javascript:;"
56-
id="comicReadMode"
57-
class="btn btn-secondary"
58-
onClick={() => showComic()}
59-
>
60-
{/* eslint-disable-next-line i18next/no-literal-string */}
61-
<i class="fa fa-book" /> Read
62-
</a>
63-
) as HTMLAnchorElement;
64-
document.getElementById('download')!.after(comicReadModeDom);
65-
66-
const enableDetectAd =
67-
options.detect_ad && querySelector('#tags .tag.tag-144644');
86+
const comicReadModeDom = createComicReadMode();
87+
const downloadBtn = document.getElementById('download');
88+
if (downloadBtn) downloadBtn.after(comicReadModeDom);
89+
else document.body.append(comicReadModeDom);
90+
91+
const shouldDetectAd = options.detect_ad;
92+
const adTagSelector = '#tags a[href="/tag/extraneous-ads/"]';
93+
const detectedTag = shouldDetectAd
94+
? await wait(
95+
() => querySelector(adTagSelector),
96+
1000,
97+
)
98+
: null;
99+
const enableDetectAd = Boolean(detectedTag);
68100
if (enableDetectAd) {
69101
setState('comicMap', '', 'adList', new ReactiveSet());
70102

71103
// 先使用缩略图识别
72104
await getAdPageByContent(
73105
querySelectorAll<HTMLImageElement>('.thumb-container img').map(
74-
(img) => img.dataset.src,
106+
(img) => img.dataset.src || img.src,
75107
),
76108
store.comicMap[''].adList!,
77109
);
@@ -100,92 +132,111 @@ declare const _gallery: { num_pages: number; media_id: string; images: Images };
100132
return styleList.join('\n');
101133
});
102134
}
135+
};
103136

104-
return;
105-
}
137+
const setupListPage = async () => {
138+
currentGalleryId = null;
139+
removeComicReadMode();
140+
141+
// 在漫画浏览页
142+
if (!document.getElementsByClassName('gallery').length) return;
143+
if (location.pathname.startsWith('/g/')) return;
106144

107-
// 在漫画浏览页
108-
if (document.getElementsByClassName('gallery').length > 0) {
109145
if (options.open_link_new_page)
110146
for (const e of querySelectorAll('a:not([href^="javascript:"])'))
111147
e.setAttribute('target', '_blank');
112148

113-
const blacklist: number[] = (unsafeWindow?._n_app ?? unsafeWindow?.n)
114-
?.options?.blacklisted_tags;
115-
if (blacklist === undefined)
149+
const app = await wait(
150+
() => (window as any)._n_app ?? (window as any).n,
151+
2000,
152+
100,
153+
);
154+
const blacklist: number[] | null | undefined = app?.options?.blacklisted_tags;
155+
156+
if (typeof blacklist === 'undefined' && app !== undefined)
116157
toast.error(t('site.nhentai.tag_blacklist_fetch_failed'));
117-
// blacklist === null 时是未登录
118158

119-
if (options.block_totally && blacklist?.length)
159+
if (options.block_totally && Array.isArray(blacklist) && blacklist.length)
120160
useStyle('.blacklisted.gallery { display: none; }');
121161

122-
if (options.auto_page_turn) {
123-
let nextUrl = querySelector<HTMLAnchorElement>('a.next')?.href;
124-
if (!nextUrl) return;
125-
126-
useStyle(`
127-
hr { bottom: 1px; box-sizing: border-box; margin: -1em auto 2em; }
128-
hr:last-child { position: relative; animation: load .8s linear alternate infinite; }
129-
hr:not(:last-child) { display: none; }
130-
@keyframes load { 0% { width: 100%; } 100% { width: 0; } }
131-
`);
132-
133-
const blackSet = new Set(blacklist);
134-
const contentDom = document.getElementById('content')!;
135-
const getObserveDom = () =>
136-
contentDom.querySelector(
137-
':is(.index-container, #favcontainer):last-of-type',
162+
const blackSet = new Set(Array.isArray(blacklist) ? blacklist : []);
163+
const contentDom = document.getElementById('content')!;
164+
let nextUrl = querySelector<HTMLAnchorElement>('a.next')?.href;
165+
const getObserveDom = () =>
166+
contentDom.querySelector(
167+
':is(.index-container, #favcontainer):last-of-type',
168+
)!;
169+
170+
const loadNextPage = singleThreaded(
171+
async (): Promise<void> => {
172+
if (!nextUrl) return;
173+
174+
const res = await request(nextUrl, {
175+
fetch: true,
176+
errorText: t('site.nhentai.fetch_next_page_failed'),
177+
});
178+
const html = domParse(res.responseText);
179+
history.replaceState(null, '', nextUrl);
180+
181+
const container = html.querySelector(
182+
'.index-container, #favcontainer',
138183
)!;
139-
140-
const loadNextPage = singleThreaded(
141-
async (): Promise<void> => {
142-
if (!nextUrl) return;
143-
144-
const res = await request(nextUrl, {
145-
fetch: true,
146-
errorText: t('site.nhentai.fetch_next_page_failed'),
147-
});
148-
const html = domParse(res.responseText);
149-
history.replaceState(null, '', nextUrl);
150-
151-
const container = html.querySelector(
152-
'.index-container, #favcontainer',
153-
)!;
154-
for (const galleryDom of container.querySelectorAll<HTMLElement>(
155-
'.gallery',
156-
)) {
157-
for (const img of galleryDom.getElementsByTagName('img'))
158-
img.setAttribute('src', img.dataset.src!);
159-
160-
// 判断是否有黑名单标签
161-
const tags = galleryDom.dataset.tags!.split(' ').map(Number);
162-
if (tags.some((tag) => blackSet.has(tag)))
163-
galleryDom.classList.add('blacklisted');
184+
for (const galleryDom of container.querySelectorAll<HTMLElement>(
185+
'.gallery',
186+
)) {
187+
for (const img of galleryDom.getElementsByTagName('img')) {
188+
const src = img.dataset.src || img.src;
189+
if (src) img.setAttribute('src', src);
164190
}
165191

166-
const pagination = html.querySelector<HTMLElement>('.pagination')!;
167-
nextUrl = pagination.querySelector<HTMLAnchorElement>('a.next')?.href;
192+
const tags = galleryDom.dataset.tags!.split(' ').map(Number);
193+
if (tags.some((tag) => blackSet.has(tag)))
194+
galleryDom.classList.add('blacklisted');
195+
}
168196

169-
contentDom.append(container, pagination);
197+
const pagination = html.querySelector<HTMLElement>('.pagination')!;
198+
nextUrl = pagination.querySelector<HTMLAnchorElement>('a.next')?.href;
170199

171-
const hr = document.createElement('hr');
172-
contentDom.append(hr);
173-
observer.disconnect();
174-
observer.observe(getObserveDom());
175-
if (!nextUrl) hr.style.animationPlayState = 'paused';
176-
},
177-
{ abandon: true },
178-
);
179-
180-
loadNextPage();
200+
contentDom.append(container, pagination);
181201

182-
const observer = new IntersectionObserver(
183-
(entries) => entries[0].isIntersecting && loadNextPage(),
184-
);
185-
observer.observe(getObserveDom());
186-
187-
if (querySelector('section.pagination'))
188-
contentDom.append(document.createElement('hr'));
202+
const hr = document.createElement('hr');
203+
contentDom.append(hr);
204+
observer.disconnect();
205+
observer.observe(getObserveDom());
206+
if (!nextUrl) hr.style.animationPlayState = 'paused';
207+
},
208+
{ abandon: true },
209+
);
210+
211+
loadNextPage();
212+
213+
const observer = new IntersectionObserver(
214+
(entries) => entries[0].isIntersecting && loadNextPage(),
215+
);
216+
observer.observe(getObserveDom());
217+
218+
if (querySelector('section.pagination'))
219+
contentDom.append(document.createElement('hr'));
220+
};
221+
222+
const processPage = async () => {
223+
const galleryPathMatch = location.pathname.match(/^\/g\/(\d+)/);
224+
if (galleryPathMatch) {
225+
await setupDetailPage(galleryPathMatch[1]);
226+
} else {
227+
await setupListPage();
228+
}
229+
};
230+
231+
await processPage();
232+
onUrlChange(async (_, nowUrl) => {
233+
const match = new URL(nowUrl).pathname.match(/^\/g\/(\d+)/);
234+
if (match) {
235+
await setupDetailPage(match[1]);
236+
} else {
237+
await setupListPage();
189238
}
190-
}
239+
});
240+
241+
return;
191242
})().catch((error) => log.error(error));

0 commit comments

Comments
 (0)