新增验车功能

This commit is contained in:
lzyme.dev@gmail.com
2026-08-31 13:50:35 +08:00
parent ce6cedf164
commit 6c9a4743bb
2 changed files with 459 additions and 411 deletions
+200 -181
View File
@@ -119,10 +119,9 @@ class MagnetHubPlugin extends BasePlugin {
}, {
name: "U3C3",
id: "u3c3",
url: "https://u3c3.com/?search2={search2}&search={keyword}",
targetPage: "https://u3c3.com/?search2={search2}&search={keyword}",
parseHtml: this.parseU3C3,
dynamicSearch2: true
url: "https://u3c3.com/?search={keyword}",
targetPage: "https://u3c3.com/?search={keyword}",
parseHtml: this.parseU3C3
}, {
name: "BTSearch",
id: "btsearch",
@@ -256,7 +255,7 @@ class MagnetHubPlugin extends BasePlugin {
color: #243b67;
font-weight: 650;
text-decoration: none;
line-height: 1;
line-height: 1.5;
}
.magnet-name-cell > a:hover {
color: #1d4ed8;
@@ -270,7 +269,7 @@ class MagnetHubPlugin extends BasePlugin {
border-radius: 3px;
font-size: 11px;
font-weight: 800;
line-height: 1;
line-height: 1.5;
color: #fff;
}
.magnet-badge.mg-subtitle { background: #16a34a; }
@@ -345,7 +344,7 @@ class MagnetHubPlugin extends BasePlugin {
.whatslink-overlay {
position: fixed;
inset: 0;
z-index: 10000040;
z-index: 2147483700;
display: flex;
align-items: center;
justify-content: center;
@@ -592,10 +591,6 @@ class MagnetHubPlugin extends BasePlugin {
sessionStorage.getItem(cacheKey);
this.resolveUrl(engine, keyword).then((url => {
const requestOpts = {method: "GET", url: url};
if (engine.dynamicSearch2) {
const cookie = sessionStorage.getItem("jhs_u3c3_cookie");
if (cookie) requestOpts.cookie = cookie;
}
if (engine.parseHtml) {
GM_xmlhttpRequest(Object.assign(requestOpts, {
method: "GET",
@@ -687,156 +682,179 @@ class MagnetHubPlugin extends BasePlugin {
}
async resolveUrl(engine, keyword) {
let url = engine.url.replace("{keyword}", encodeURIComponent(keyword));
if (engine.dynamicSearch2) {
let search2 = sessionStorage.getItem("jhs_u3c3_search2");
if (!search2) {
search2 = await this.fetchSearch2();
if (search2) sessionStorage.setItem("jhs_u3c3_search2", search2);
}
url = url.replace("{search2}", search2 || "c2pvbmy2");
}
return url;
}
async fetchSearch2() {
const result = await new Promise((resolve => {
GM_xmlhttpRequest({
method: "GET",
url: "https://u3c3.com/",
onload: r => resolve({html: r.responseText, headers: r.responseHeaders || ""}),
onerror: () => resolve(null)
});
}));
if (!result) return null;
const cookie = parseSetCookie(result.headers);
if (cookie) sessionStorage.setItem("jhs_u3c3_cookie", cookie);
const match = result.html.match(/var\s+nmefafej\s*=\s*"([^"]+)"/g);
if (!match) return null;
for (const m of match) {
const val = m.match(/"([^"]+)"/)[1];
if (val.length === 8) {
console.log("[U3C3] search2:", val);
return val;
}
}
return null;
return engine.url.replace("{keyword}", encodeURIComponent(keyword));
}
async checkWhatslink(magnet) {
document.querySelector('.whatslink-overlay')?.remove();
const overlay = document.createElement('div');
overlay.className = 'whatslink-overlay';
overlay.innerHTML = '<div class="whatslink-loading">正在加载验车数据…</div>';
overlay.addEventListener('click', e => {
if (e.target === overlay) overlay.remove();
});
overlay.innerHTML = '<div class="whatslink-modal no-shots"><div class="whatslink-loading">正在验车...</div></div>';
document.body.appendChild(overlay);
try {
const url = `https://whatslink.info/api/v1/link?url=${encodeURIComponent(magnet)}`;
const r = await gmFetch(url);
if (!r.loadstuts) throw new Error('查询失败');
const data = JSON.parse(r.responseText);
this.showWhatslinkModal(overlay, data, magnet);
const r = await gmFetch(url, {timeout: 20000});
if (!r.loadstuts) throw new Error('WhatsLink 请求失败');
const data = JSON.parse(r.responseText || '{}');
overlay.remove();
this.showWhatslinkModal(data, magnet);
} catch (e) {
overlay.innerHTML = `<div class="whatslink-gallery-scene">
<div class="whatslink-gallery-visual">
<div class="whatslink-gallery-empty">
<div class="whatslink-gallery-empty-icon">⚠</div>
<div class="whatslink-gallery-empty-title">验车失败</div>
<div class="whatslink-gallery-empty-text">${e.message || '无法获取文件信息'}</div>
</div>
</div>
</div>`;
overlay.querySelector('.whatslink-overlay')?.classList.add('no-shots');
overlay.remove();
this.showWhatslinkModal({
error: e.message || '查询失败',
name: '查询失败',
type: '-',
file_type: '-',
size: 0,
count: '-',
screenshots: []
}, magnet);
}
}
showWhatslinkModal(overlay, data, magnet) {
const screenshots = Array.isArray(data.screenshots) ? data.screenshots : [];
const hasShots = screenshots.length > 0;
const sizeStr = data.size ? formatBytes(data.size) : '-';
const fileCount = data.count || '-';
overlay.innerHTML = '';
const modal = document.createElement('div');
modal.className = 'whatslink-modal' + (hasShots ? '' : ' no-shots');
const scene = document.createElement('div');
scene.className = 'whatslink-gallery-scene';
const visual = document.createElement('div');
visual.className = 'whatslink-gallery-visual';
const closeBtn = document.createElement('button');
closeBtn.className = 'whatslink-gallery-close';
closeBtn.innerHTML = '×';
closeBtn.addEventListener('click', () => overlay.remove());
visual.appendChild(closeBtn);
let currentIdx = 0;
const hero = document.createElement('img');
hero.className = 'whatslink-gallery-hero';
if (hasShots) {
hero.src = screenshots[0];
visual.appendChild(hero);
const prevBtn = document.createElement('button');
prevBtn.className = 'whatslink-gallery-arrow whatslink-gallery-prev';
prevBtn.innerHTML = '‹';
prevBtn.addEventListener('click', e => {
e.stopPropagation();
currentIdx = (currentIdx - 1 + screenshots.length) % screenshots.length;
hero.src = screenshots[currentIdx];
updateThumbs();
updateInfo();
});
visual.appendChild(prevBtn);
const nextBtn = document.createElement('button');
nextBtn.className = 'whatslink-gallery-arrow whatslink-gallery-next';
nextBtn.innerHTML = '›';
nextBtn.addEventListener('click', e => {
e.stopPropagation();
currentIdx = (currentIdx + 1) % screenshots.length;
hero.src = screenshots[currentIdx];
updateThumbs();
updateInfo();
});
visual.appendChild(nextBtn);
} else {
const emptyDiv = document.createElement('div');
emptyDiv.className = 'whatslink-gallery-empty';
emptyDiv.innerHTML = '<div class="whatslink-gallery-empty-icon">🖼</div><div class="whatslink-gallery-empty-title">暂无预览图</div><div class="whatslink-gallery-empty-text">该磁力链接暂无截图信息</div>';
visual.appendChild(emptyDiv);
}
const info = document.createElement('div');
info.className = 'whatslink-gallery-info';
const updateInfo = () => {
info.innerHTML = `<span>${currentIdx + 1} / ${screenshots.length}</span><span>${sizeStr} · ${fileCount} 文件</span>`;
};
if (hasShots) updateInfo();
else info.innerHTML = `<span>${sizeStr} · ${fileCount} 文件</span>`;
scene.appendChild(visual);
scene.appendChild(info);
if (hasShots && screenshots.length > 1) {
const thumbs = document.createElement('div');
thumbs.className = 'whatslink-gallery-thumbs';
const updateThumbs = () => {
thumbs.innerHTML = '';
screenshots.forEach((src, i) => {
const thumb = document.createElement('button');
thumb.className = 'whatslink-gallery-thumb' + (i === currentIdx ? ' active' : '');
const img = document.createElement('img');
img.src = src;
thumb.appendChild(img);
thumb.addEventListener('click', e => {
e.stopPropagation();
currentIdx = i;
hero.src = src;
updateThumbs();
updateInfo();
});
thumbs.appendChild(thumb);
});
};
updateThumbs();
scene.appendChild(thumbs);
}
modal.appendChild(scene);
showWhatslinkModal(payload, magnet) {
document.querySelector('.whatslink-overlay')?.remove();
const shots = Array.isArray(payload?.screenshots) ? payload.screenshots.map(item => item?.screenshot).filter(Boolean) : [];
let index = 0;
const resourceName = payload?.name || '未知资源';
const resourceCount = payload?.count ?? '-';
const overlay = document.createElement('div');
overlay.className = 'whatslink-overlay';
const modal = document.createElement('section');
modal.className = `whatslink-modal${shots.length ? '' : ' no-shots'}`;
modal.innerHTML = `<div class="whatslink-gallery-scene">
<div class="whatslink-gallery-visual">
<button class="whatslink-gallery-close" type="button" aria-label="关闭">×</button>
<button class="whatslink-gallery-arrow whatslink-gallery-prev" type="button" aria-label="上一张">‹</button>
<img class="whatslink-gallery-hero" alt="截图预览">
<button class="whatslink-gallery-arrow whatslink-gallery-next" type="button" aria-label="下一张">›</button>
<div class="whatslink-gallery-empty">
<div class="whatslink-gallery-empty-icon">?</div>
<div class="whatslink-gallery-empty-title">暂无截图</div>
<p class="whatslink-gallery-empty-text">当前资源没有可展示的截图,可以结合资源名称和文件数量判断。</p>
</div>
</div>
<div class="whatslink-gallery-info">
<span class="whatslink-gallery-name"></span>
<span class="whatslink-gallery-count"></span>
<span class="whatslink-gallery-index"></span>
</div>
<div class="whatslink-gallery-thumbs"></div>
</div>`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
modal.querySelector('.whatslink-gallery-name').textContent = resourceName;
modal.querySelector('.whatslink-gallery-count').textContent = `${resourceCount} 个文件`;
const visual = modal.querySelector('.whatslink-gallery-visual');
const hero = modal.querySelector('.whatslink-gallery-hero');
const thumbs = modal.querySelector('.whatslink-gallery-thumbs');
const currentIndex = modal.querySelector('.whatslink-gallery-index');
const closeButton = modal.querySelector('.whatslink-gallery-close');
const prevButton = modal.querySelector('.whatslink-gallery-prev');
const nextButton = modal.querySelector('.whatslink-gallery-next');
const sizeGallery = () => {
if (!shots.length || !hero.naturalWidth || !hero.naturalHeight) return;
const infoHeight = modal.querySelector('.whatslink-gallery-info').offsetHeight || 23;
const thumbsHeight = thumbs.offsetHeight || 58;
const maxHeight = Math.max(180, window.innerHeight - 36 - infoHeight - thumbsHeight - 6);
const maxWidth = modal.clientWidth;
const ratio = hero.naturalWidth / hero.naturalHeight;
const height = Math.min(maxHeight, maxWidth / ratio);
const width = height * ratio;
visual.style.width = `${width}px`;
visual.style.height = `${height}px`;
};
const positionControls = () => {
if (!shots.length || !hero.naturalWidth || !hero.naturalHeight) return;
const width = visual.clientWidth;
const height = visual.clientHeight;
const scale = Math.min(width / hero.naturalWidth, height / hero.naturalHeight);
const imageWidth = hero.naturalWidth * scale;
const imageHeight = hero.naturalHeight * scale;
const imageLeft = (width - imageWidth) / 2;
const imageTop = (height - imageHeight) / 2;
const closeSize = closeButton.offsetWidth || 32;
const arrowSize = prevButton.offsetWidth || 34;
const sideGap = 12;
closeButton.style.left = `${imageLeft + imageWidth - closeSize - sideGap}px`;
closeButton.style.right = 'auto';
closeButton.style.top = `${imageTop + sideGap}px`;
prevButton.style.left = `${imageLeft + sideGap}px`;
prevButton.style.right = 'auto';
prevButton.style.top = `${imageTop + (imageHeight - arrowSize) / 2}px`;
nextButton.style.left = 'auto';
nextButton.style.right = `${width - imageLeft - imageWidth + sideGap}px`;
nextButton.style.top = `${imageTop + (imageHeight - arrowSize) / 2}px`;
};
const render = () => {
modal.classList.toggle('has-shots', Boolean(shots.length));
if (!shots.length) {
currentIndex.textContent = 'NO PREVIEW';
return;
}
hero.src = shots[index];
const frame = `${index + 1} / ${shots.length}`;
currentIndex.textContent = frame;
[...thumbs.children].forEach((btn, i) => btn.classList.toggle('active', i === index));
if (hero.complete) {
sizeGallery();
positionControls();
}
};
shots.forEach((url, i) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'whatslink-gallery-thumb';
btn.innerHTML = `<img src="${url}" alt="截图${i + 1}">`;
btn.addEventListener('click', () => {
index = i;
render();
});
thumbs.appendChild(btn);
});
prevButton.addEventListener('click', () => {
if (!shots.length) return;
index = (index + shots.length - 1) % shots.length;
render();
});
nextButton.addEventListener('click', () => {
if (!shots.length) return;
index = (index + 1) % shots.length;
render();
});
const onKeydown = event => {
if (!document.body.contains(overlay)) return;
if (event.key === 'ArrowLeft' && shots.length) {
index = (index + shots.length - 1) % shots.length;
render();
}
if (event.key === 'ArrowRight' && shots.length) {
index = (index + 1) % shots.length;
render();
}
if (event.key === 'Escape') close();
};
const onResize = () => {
sizeGallery();
positionControls();
};
const removeResize = () => window.removeEventListener('resize', onResize);
const close = () => {
overlay.remove();
document.removeEventListener('keydown', onKeydown);
removeResize();
};
closeButton.addEventListener('click', close);
overlay.addEventListener('click', e => {
if (e.target === overlay) close();
});
hero.addEventListener('load', () => {
sizeGallery();
positionControls();
});
window.addEventListener('resize', onResize);
document.addEventListener('keydown', onKeydown);
render();
}
parseU9A9(html, keyword) {
@@ -860,25 +878,40 @@ class MagnetHubPlugin extends BasePlugin {
return results;
}
parseU3C3(html, keyword) {
const $dom = utils.htmlTo$dom(html), results = [];
const kwNorm = String(keyword).toUpperCase().replace(/[^A-Z0-9]/g, '');
$dom.find(".torrent-list tbody tr").each(((i, el) => {
const $el = $(el);
if ($el.text().includes("置顶")) return;
const $titleA = $el.find("td:nth-child(2) a");
const title = $titleA.attr("title") || $titleA.text().trim();
if (!title) return;
if (kwNorm) {
const titleNorm = title.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (!titleNorm.includes(kwNorm)) return;
}
const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href");
const size = $el.find("td:nth-child(4)").text().trim();
const date = $el.find("td:nth-child(5)").text().trim();
magnet && results.push({title, magnet, size, date});
}));
return results;
async parseU3C3(_html, keyword) {
const base = 'https://www.u3c3.com';
const referer = base + '/';
const home = await gmFetch(base + '/', {
headers: {Referer: referer},
});
if (!home.loadstuts) return [];
const homeDoc = parseHTML(home.responseText);
const searchScript = [...homeDoc.scripts].map(script => script.textContent || '').find(text => text.includes('function search21')) || '';
const token = searchScript.match(/^\s*var\s+nmefafej\s*=\s*["']([^"']+)["'];?/m)?.[1] || '';
if (!token) return [];
const searchUrl = `${base}/?search2=${encodeURIComponent(token)}&search=${encodeURIComponent(keyword)}`;
const r = await gmFetch(searchUrl, {
headers: {Referer: referer},
});
if (!r.loadstuts) return [];
const doc = parseHTML(r.responseText);
const normalizedKeyword = String(keyword || '').toUpperCase().replace(/[-_\s]/g, '');
const data = [...doc.querySelectorAll('table.torrent-list tbody tr.default, table.torrent-list tbody tr.success')].map(row => {
const titleA = row.querySelector('td:nth-child(2) a[href*="/view?id="]');
const magnetA = row.querySelector('td:nth-child(3) a[href^="magnet:"]');
const title = titleA?.getAttribute('title')?.trim() || titleA?.textContent?.trim() || '';
if (!title || !magnetA?.href) return null;
if (normalizedKeyword && !title.toUpperCase().replace(/[-_\s]/g, '').includes(normalizedKeyword)) return null;
const href = titleA.getAttribute('href') || '';
return {
title,
magnet: magnetA.href,
size: row.querySelector('td:nth-child(4)')?.textContent?.trim() || '',
date: row.querySelector('td:nth-child(5)')?.textContent?.trim() || '',
src: href ? new URL(href, base).href : searchUrl,
};
}).filter(Boolean);
return data;
}
parseSukebei(html, keyword) {
@@ -1006,20 +1039,6 @@ class MagnetHubPlugin extends BasePlugin {
}
}
function parseSetCookie(headers) {
if (!headers) return "";
const lines = headers.split("\n");
const cookies = [];
for (const line of lines) {
const match = line.match(/^set-cookie:\s*(.+?)(?:;|$)/i);
if (match) {
const val = match[1].split(";")[0].trim();
if (val) cookies.push(val);
}
}
return cookies.join("; ");
}
function formatBytes(bytes) {
if (!bytes || bytes <= 0) return '';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];