mirror of
https://github.com/fireinrain/JHS-enhance.git
synced 2026-10-02 08:56:38 +08:00
新增验车功能
This commit is contained in:
+200
-181
@@ -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'];
|
||||
|
||||
Reference in New Issue
Block a user