From 2d2816363b05a18782a854f7d042f4d5ae66b61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E4=BB=A3=E7=B6=BA=E5=87=9B?= Date: Mon, 29 Jul 2024 01:09:12 +0800 Subject: [PATCH] fix: gallery mini popover on mirror sites --- src/app/GalleryMiniPopover.vue | 25 +++++-- src/const.ts | 2 + src/utils/nhentai.ts | 126 ++++++++++++++++++++++++--------- 3 files changed, 114 insertions(+), 39 deletions(-) diff --git a/src/app/GalleryMiniPopover.vue b/src/app/GalleryMiniPopover.vue index 3e1014b..150c584 100644 --- a/src/app/GalleryMiniPopover.vue +++ b/src/app/GalleryMiniPopover.vue @@ -43,7 +43,8 @@ > @@ -113,6 +114,7 @@ import { getGallery, type NHentaiGallery, type NHentaiImage, + getCompliedThumbMediaUrlTemplate, } from '@/utils/nhentai'; import { settings } from '@/utils/settings'; import logger from '@/utils/logger'; @@ -120,7 +122,15 @@ import logger from '@/utils/logger'; const POPOVER_MAX_WIDTH = 720; const POPOVER_THUMB_MORE_COL_WIDTH = 640; -const TAG_TYPES = ['parody', 'character', 'tag', 'artist', 'language', 'category'] as const; +const TAG_TYPES = [ + 'parody', + 'character', + 'tag', + 'artist', + 'group', + 'language', + 'category', +] as const; const getTagSortIndex = (type: string) => { const index = TAG_TYPES.findIndex(t => t === type); return index === -1 ? 999 : index; @@ -150,7 +160,7 @@ const groupedTags = computed(() => { : []; }); -const galleryLink = computed(() => `https://nhentai.net/g/${gallery.value?.id}/`); +const galleryLink = computed(() => `${location.origin}/g/${gallery.value?.id}/`); const pageThumbs = ref>([]); const pageThumbsColSpan = computed(() => @@ -175,8 +185,12 @@ const limitTagLength = (tags: NHentaiTag[], maxLength: number) => { const isLimitTag = (tag: NHentaiTag) => tag.type === '__limit__'; +let thumbUrlTemplate: Awaited>; const getThumbInfo = ({ t, w, h }: NHentaiImage, i: number) => ({ - url: `https://t3.nhentai.net/galleries/${gallery.value?.media_id}/${i + 1}t.${NHentaiImgExt[t]}`, + url: thumbUrlTemplate({ + mid: gallery.value?.media_id, + filename: `${i + 1}t.${NHentaiImgExt[t]}`, + }), height: w && h ? Math.floor(pageThumbWidth.value * Math.min(h / w, 1.8)) : 0, }); @@ -188,7 +202,7 @@ const formatNumber = (num: number) => { const openTagUrl = (path?: string) => { if (!path) return; - GM_openInTab(`https://nhentai.net${path}`, { active: true, setParent: true }); + GM_openInTab(`${location.origin}${path}`, { active: true, setParent: true }); }; let loadingGid: string = ''; @@ -209,6 +223,7 @@ const open = async (el: HTMLElement, gid: string) => { pageThumbs.value = []; try { loadingGid = gid; + if (!thumbUrlTemplate) thumbUrlTemplate = await getCompliedThumbMediaUrlTemplate(); const loadedGallery = await getGallery(gid); if (loadingGid !== gid) return; gallery.value = loadedGallery; diff --git a/src/const.ts b/src/const.ts index 42db470..c8dd103 100644 --- a/src/const.ts +++ b/src/const.ts @@ -27,3 +27,5 @@ export const IS_NHENTAI = host === 'nhentai.net'; export const IS_NHENTAI_TO = host === 'nhentai.to' || host === 'nhentai.website'; export const MEDIA_URL_TEMPLATE_KEY = `media_url_template_${host}`; + +export const THUMB_MEDIA_URL_TEMPLATE_KEY = `thumb_media_url_template_${host}`; diff --git a/src/utils/nhentai.ts b/src/utils/nhentai.ts index 31cf62c..0e0ebfc 100644 --- a/src/utils/nhentai.ts +++ b/src/utils/nhentai.ts @@ -14,7 +14,12 @@ import logger from './logger'; import { Counter } from './counter'; import { loadHTML } from './html'; import { OrderCache } from './orderCache'; -import { IS_NHENTAI, IS_PAGE_MANGA_DETAIL, MEDIA_URL_TEMPLATE_KEY } from '@/const'; +import { + IS_NHENTAI, + IS_PAGE_MANGA_DETAIL, + MEDIA_URL_TEMPLATE_KEY, + THUMB_MEDIA_URL_TEMPLATE_KEY, +} from '@/const'; export enum NHentaiImgExt { j = 'jpg', @@ -106,30 +111,39 @@ const getGalleryFromApi = (gid: number | string): Promise => { return fetchJSON(url); }; +const fixGalleryObj = (gallery: NHentaiGallery, gid?: number | string) => { + // 有些镜像站有™大病,gallery 信息里的 id 是错误的,以地址为准 + if (gid) gallery.id = Number(gid); + // 有些镜像站的图片列表是个 object,需要特殊处理 + if (!Array.isArray(gallery.images.pages)) { + gallery.images.pages = Object.values(gallery.images.pages); + } + return gallery; +}; + const getGalleryFromWebpage = async (gid: number | string): Promise => { let doc = document; if (!IS_PAGE_MANGA_DETAIL) { const html = await getText(`/g/${gid}`); - - // 有些站点 script 里有 gallery 信息 - const match = /gallery(\(\{[\s\S]+\}\));/.exec(html)?.[1]; - if (match) { - try { - // eslint-disable-next-line no-eval - const gallery: NHentaiGallery = eval(match); - // 有些镜像站有™大病,gallery 信息里的 id 是错误的,以地址为准 - gallery.id = Number(gid); - } catch { - logger.warn('get gallery by eval failed'); - } - } - // 直接把 html 给 jq 解析的话会把里面的图片也给加载了,用 DOMParser 解析完再扔给 jq 就不会 const parser = new DOMParser(); doc = parser.parseFromString(html, 'text/html'); } + // 有些站点 script 里有 gallery 信息 + const match = /gallery(\(\{[\s\S]+\}\));/.exec(doc.body.innerHTML)?.[1]; + if (match) { + try { + // eslint-disable-next-line no-eval + const gallery: NHentaiGallery = eval(match); + logger.log('get gallery by script tag success'); + return fixGalleryObj(gallery, gid); + } catch { + logger.warn('get gallery by script tag failed'); + } + } + const $doc = $(doc.body); const english = $doc.find('#info h1').text(); @@ -140,13 +154,19 @@ const getGalleryFromWebpage = async (gid: number | string): Promise('#thumbnail-container img').each((i, img) => { const src = img.dataset.src ?? img.src; + const width = img.getAttribute('width'); + const height = img.getAttribute('height'); const match = /\/(\d+)\/(\d+)t?\.(\w+)/.exec(src); if (!match) return; const [, mid, index, ext] = match; if (!mediaId) mediaId = mid; const t = getTypeFromExt(ext); if (!t) return; - pages[Number(index) - 1] = { t }; + pages[Number(index) - 1] = { + t, + w: width ? Number(width) : undefined, + h: height ? Number(height) : undefined, + }; }); if ((!english && !japanese) || !mediaId || !pages.length) { @@ -154,9 +174,21 @@ const getGalleryFromWebpage = async (gid: number | string): Promise { - const $names = $(`#tags .tag-container:contains(${elContains}) .tag > .name`); - const names = filter(Array.from($names).map(el => el.innerText.trim())); - return names.map((name): NHentaiTag => ({ type, name })); + const $tags = $doc.find(`#tags .tag-container:contains(${elContains}) .tag`); + return filter( + Array.from($tags).map((el): NHentaiTag | undefined => { + const name = el.querySelector('.name')?.innerText.trim(); + const count = el.querySelector('.count')?.innerText.trim(); + return name + ? { + type, + name, + url: el.getAttribute('href') || undefined, + count: count ? Number(count) : undefined, + } + : undefined; + }), + ) as NHentaiTag[]; }; const tags = [ @@ -169,7 +201,7 @@ const getGalleryFromWebpage = async (gid: number | string): Promise(pages)).map( - ({ t, w, h }, i) => ({ i: i + 1, t: NHentaiImgExt[t], w, h }), - ); + const infoPages = pages.map(({ t, w, h }, i) => ({ i: i + 1, t: NHentaiImgExt[t], w, h })); const info: NHentaiGalleryInfo = { gid: id, @@ -291,14 +315,38 @@ const fetchMediaUrlTemplate = async () => { return template; }; -const getMediaUrlTemplate = async () => { +const fetchThumbMediaUrlTemplate = async () => { + const detailUrl = document.querySelector('.gallery a')?.getAttribute('href'); + if (!detailUrl) { + throw new Error('get detail url failed: cannot find a gallery'); + } + + logger.log(`fetching thumb media url template by ${detailUrl}`); + + const detailHtml = await getText(detailUrl); + const $doc = loadHTML(detailHtml); + const $img = $doc.find('#thumbnail-container img'); + const imgSrc = $img.attr('data-src') || $img.attr('src'); + if (!imgSrc) { + throw new Error('get thumb media url failed: cannot find an image src'); + } + + const template = imgSrc + .replace(/\/\d+\//, '/{{mid}}/') + .replace(/\/\d+t\.[^/]+$/, '/{{filename}}'); + GM_setValue(THUMB_MEDIA_URL_TEMPLATE_KEY, template); + + return template; +}; + +const getMediaUrlTemplate = async (getter: () => Promise, cacheKey: string) => { try { - const template = await fetchMediaUrlTemplate(); + const template = await getter(); logger.log(`use media url template: ${template}`); return template; } catch (error) { logger.error(error); - const cachedTemplate = GM_getValue(MEDIA_URL_TEMPLATE_KEY); + const cachedTemplate = GM_getValue(cacheKey); if (cachedTemplate) { logger.warn(`try to use cached media url template: ${cachedTemplate}`); return cachedTemplate; @@ -307,7 +355,17 @@ const getMediaUrlTemplate = async () => { } }; -const getCompliedMediaUrlTemplate = once(async () => compileTemplate(await getMediaUrlTemplate())); +const getCompliedMediaUrlTemplate = once(async () => + compileTemplate(await getMediaUrlTemplate(fetchMediaUrlTemplate, MEDIA_URL_TEMPLATE_KEY)), +); + +export const getCompliedThumbMediaUrlTemplate = once(async () => + compileTemplate( + IS_NHENTAI + ? 'https://t3.nhentai.net/galleries/{{mid}}/{{filename}}' + : await getMediaUrlTemplate(fetchThumbMediaUrlTemplate, THUMB_MEDIA_URL_TEMPLATE_KEY), + ), +); const applyTitleReplacement = (title: string) => { if (!validTitleReplacement.value.length) return title;