Skip to content

Commit

Permalink
fix: gallery mini popover on mirror sites
Browse files Browse the repository at this point in the history
  • Loading branch information
Tsuk1ko committed Jul 28, 2024
1 parent 27d5515 commit 2d28163
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 39 deletions.
25 changes: 20 additions & 5 deletions src/app/GalleryMiniPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
>
<template v-if="isLimitTag(tag)">+{{ tag.count }}</template>
<template v-else>
<span class="bold">{{ tag.name }}</span> | {{ formatNumber(tag.count || 0) }}
<span class="bold">{{ tag.name }}</span
>{{ tag.count ? ` | ${formatNumber(tag.count)}` : undefined }}
</template>
</el-tag>
</el-descriptions-item>
Expand Down Expand Up @@ -113,14 +114,23 @@ import {
getGallery,
type NHentaiGallery,
type NHentaiImage,
getCompliedThumbMediaUrlTemplate,
} from '@/utils/nhentai';
import { settings } from '@/utils/settings';
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;
Expand Down Expand Up @@ -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<Array<{ url: string; height: number }>>([]);
const pageThumbsColSpan = computed(() =>
Expand All @@ -175,8 +185,12 @@ const limitTagLength = (tags: NHentaiTag[], maxLength: number) => {
const isLimitTag = (tag: NHentaiTag) => tag.type === '__limit__';
let thumbUrlTemplate: Awaited<ReturnType<typeof getCompliedThumbMediaUrlTemplate>>;
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,
});
Expand All @@ -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 = '';
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
126 changes: 92 additions & 34 deletions src/utils/nhentai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -106,30 +111,39 @@ const getGalleryFromApi = (gid: number | string): Promise<NHentaiGallery> => {
return fetchJSON<NHentaiGallery>(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<NHentaiGallery> => {
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();
Expand All @@ -140,23 +154,41 @@ const getGalleryFromWebpage = async (gid: number | string): Promise<NHentaiGalle

$doc.find<HTMLImageElement>('#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) {
throw new Error('Get gallery info error.');
}

const getTags = (type: string, elContains: string): NHentaiTag[] => {
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<HTMLElement>('.name')?.innerText.trim();
const count = el.querySelector<HTMLElement>('.count')?.innerText.trim();
return name
? {
type,
name,
url: el.getAttribute('href') || undefined,
count: count ? Number(count) : undefined,
}
: undefined;
}),
) as NHentaiTag[];
};

const tags = [
Expand All @@ -169,7 +201,7 @@ const getGalleryFromWebpage = async (gid: number | string): Promise<NHentaiGalle
...getTags('category', 'Categories'),
];

const uploadDateStr = $('#tags .tag-container:contains(Uploaded) time').attr('datetime');
const uploadDateStr = $doc.find('#tags .tag-container:contains(Uploaded) time').attr('datetime');
const uploadDate = uploadDateStr ? new Date(uploadDateStr) : undefined;

return {
Expand Down Expand Up @@ -229,23 +261,15 @@ export const getGalleryInfo = async (gid?: number | string): Promise<NHentaiGall
const gidFromUrl = /^\/g\/(\d+)/.exec(location.pathname)?.[1];
const localGallery = unsafeWindow._gallery ?? unsafeWindow.gallery;

if (localGallery) {
// 有些镜像站有™大病,gallery 信息里的 id 是错误的,以地址为准
if (gidFromUrl) localGallery.id = Number(gidFromUrl);
return localGallery;
}

if (localGallery) return fixGalleryObj(localGallery, gidFromUrl);
if (gidFromUrl) return getGallery(gidFromUrl);

throw new Error('Cannot get gallery info.');
})();

const { english, japanese, pretty } = title;

// 有些站点例如 nhentai.website 的 gallery 里面的图片列表是个 object,需要特殊处理
const infoPages = (Array.isArray(pages) ? pages : Object.values<NHentaiImage>(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,
Expand Down Expand Up @@ -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<string>, 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<string | undefined>(MEDIA_URL_TEMPLATE_KEY);
const cachedTemplate = GM_getValue<string | undefined>(cacheKey);
if (cachedTemplate) {
logger.warn(`try to use cached media url template: ${cachedTemplate}`);
return cachedTemplate;
Expand All @@ -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;
Expand Down

0 comments on commit 2d28163

Please sign in to comment.