// ==UserScript==
// @name 磁力链接悬浮预览
// @namespace http://whatslink.info/
// @version 2.5
// @description 在磁力链接后添加标识符号,通过点击或悬停显示完整链接信息
// @author sexjpg
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @connect whatslink.info
// @match *://*6v520.com*/*
// @match *://*javdb*.*/*
// @match *://*javbus*.*/*
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 配置参数
const CONFIG = {
delay: 500, // 悬浮延迟时间(毫秒)
cacheTTL: 10800 * 60 * 1000, // 缓存有效期(10800分钟)
indicator_innerhtml: '🧲'
};
// 缓存对象,使用{}
const magnetCache = GM_getValue('magnetCache', {});
// 创建悬浮框容器
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
max-width: 400px;
min-width: 300px;
padding: 15px;
background: rgba(0, 0, 0, 0.95);
color: #fff;
border-radius: 8px;
font-size: 14px;
font-family: Arial, sans-serif;
z-index: 9999;
pointer-events: auto; /* 修改为 auto,允许鼠标事件 */
word-break: break-all;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
transform: scale(0.95);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
display: none;
`;
// 新增变量用于控制 tooltip 状态
let tooltipHideTimer = null;
let isTooltipHovered = false;
document.body.appendChild(tooltip);
// 磁力链接检测正则
const magnetRegex = /^magnet:\?xt=urn:btih:([a-fA-F0-9]{40})(?:&|$)/i;
// 标识符号样式
const indicatorStyle = `
display: inline-block;
width: 16px;
height: 16px;
background: #007bff;
border-radius: 50%;
color: white;
text-align: center;
font-size: 12px;
margin-left: 4px;
cursor: help;
user-select: none;
vertical-align: middle;
transition: all 0.2s ease;
`;
// 获取磁力链接特征码
function getMagnetHash(magnetLink) {
const match = magnetLink.match(magnetRegex);
return match ? match[1].toLowerCase() : null;
}
// API请求函数(修正GET请求方式)
function fetchMagnetInfo(magnetLink, callback) {
try {
GM_xmlhttpRequest({
method: 'GET',
url: `https://whatslink.info/api/v1/link?url=${magnetLink}`,
headers: { 'Content-Type': "text/plain", },
onload: function (response) {
try {
const data = JSON.parse(response.responseText);
console.debug('网络请求数据', data);
// 只缓存有效数据,有数据,且数据无错误,且文件类型不为空
if (data && !data.error && data.file_type) {
const hash = getMagnetHash(magnetLink);
if (hash) {
magnetCache[hash] = {
data: data,
expiresAt: Date.now() + CONFIG.cacheTTL
};
// 保存缓存
console.debug('更新缓存', magnetCache[hash]);
GM_setValue('magnetCache', magnetCache);
console.debug('更新缓存完成,总缓存数量:', Object.keys(magnetCache).length);
}
}
callback(null, data);
} catch (error) {
callback(new Error('解析响应数据失败: ' + error.message));
}
},
onerror: function (error) {
callback(new Error('API请求失败: ' + error.statusText));
}
});
} catch (error) {
callback(new Error('请求异常: ' + error.message));
}
}
// 检查缓存
function checkCache(magnetLink) {
const hash = getMagnetHash(magnetLink);
console.debug('开始检索缓存,缓存总量', Object.keys(magnetCache).length, magnetCache);
console.debug('检索特征码', hash);
if (!hash || !magnetCache[hash]) {
console.debug('缓存中未检索到特征码:', hash);
return null
};
// 检查缓存是否过期
if (Date.now() > magnetCache[hash].expiresAt) {
delete magnetCache[hash];
console.debug('缓存特征码过期', hash);
return null;
}
console.debug('获取缓存数据', magnetCache[hash]);
return magnetCache[hash].data;
}
// 数据展示函数
function renderMagnetInfo(data) {
let html = `
<div style="margin-bottom: 10px;">
<strong style="font-size: 16px; word-break: break-word;">${data.name || '未知名称'}</strong>
</div>
<div style="margin-bottom: 8px;">
<span>类型:</span>
<span style="color: #17a2b8;">${data.type || '未知类型'}</span>
</div>
<div style="margin-bottom: 8px;">
<span>文件类型:</span>
<span style="color: #ffc107;">${data.file_type || '未知文件类型'}</span>
</div>
<div style="margin-bottom: 8px;">
<span>大小:</span>
<span style="color: #28a745;">${formatFileSize(data.size) || '未知大小'}</span>
</div>
<div style="margin-bottom: 8px;">
<span>文件数:</span>
<span style="color: #dc3545;">${data.count || 0}</span>
</div>
`;
if (data.screenshots && data.screenshots.length > 0) {
html += `<div style="margin-top: 15px; display: flex; flex-wrap: wrap; gap: 5px;">`;
data.screenshots.slice(0, 5).forEach(screenshot => {
html += `
<div style="flex: 1 1 45%; min-width: 100px;">
<img src="${screenshot.screenshot}"
style="width: 100%; border-radius: 4px; box-shadow: 0 2px 6px rgba(0,0,0,0.3);">
</div>
`;
});
html += `</div>`;
}
return html;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === undefined || bytes === null) return '未知大小';
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 显示悬浮框的核心逻辑
function showTooltip(magnetLink, event) {
// 检查缓存
const cachedData = checkCache(magnetLink);
if (cachedData) {
// 使用缓存数据
tooltip.innerHTML = renderMagnetInfo(cachedData);
updateTooltipPosition(event);
tooltip.style.display = 'block';
tooltip.style.opacity = '1';
tooltip.style.transform = 'scale(1)';
return;
}
// 显示加载状态
tooltip.innerHTML = '<div style="text-align: center; padding: 10px;">加载中...</div>';
tooltip.style.display = 'block';
tooltip.style.opacity = '1';
tooltip.style.transform = 'scale(1)';
updateTooltipPosition(event);
// 请求API数据
fetchMagnetInfo(magnetLink, (error, data) => {
if (error) {
tooltip.innerHTML = `<div style="color: #dc3545; text-align: center; padding: 10px;">${error.message}</div>`;
} else {
tooltip.innerHTML = renderMagnetInfo(data);
}
updateTooltipPosition(event);
});
}
// 更新悬浮框位置
function updateTooltipPosition(e) {
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
let x = e.clientX + 15;
//本身是y = e.clientY + 15,改为往上调整15个像素
let y = e.clientY - 15;
// 防止超出右侧视口
if (x + tooltipRect.width > viewportWidth - 20) {
x = e.clientX - tooltipRect.width - 15;
}
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
}
// 处理单个链接元素
function processLink(link) {
// 检查是否是磁力链接且未被处理过
if (link.dataset.magnetProcessed || !magnetRegex.test(link.href)) {
return;
}
link.dataset.magnetProcessed = 'true'; // 标记为已处理
let timer = null;
let isHovered = false; // 新增悬停状态
const indicator = document.createElement('span');
indicator.innerHTML = CONFIG.indicator_innerhtml;
indicator.style.cssText = indicatorStyle;
link.appendChild(indicator);
// 鼠标进入事件
indicator.addEventListener('mouseenter', (e) => {
clearTimeout(tooltipHideTimer); // 清除之前的隐藏计时器
timer = setTimeout(() => {
showTooltip(link.href, e);
}, CONFIG.delay);
});
indicator.addEventListener('mouseleave', () => {
clearTimeout(timer); // 取消未触发的显示
// 不再立即隐藏 tooltip,交给 tooltip 自己控制
});
indicator.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
clearTimeout(timer);
showTooltip(link.href, e);
});
tooltip.addEventListener('mouseenter', () => {
isTooltipHovered = true;
clearTimeout(tooltipHideTimer);
});
tooltip.addEventListener('mouseleave', () => {
isTooltipHovered = false;
tooltipHideTimer = setTimeout(() => {
tooltip.style.opacity = '0';
tooltip.style.transform = 'scale(0.95)';
setTimeout(() => {
tooltip.style.display = 'none';
}, 300); // 与 transition 时间匹配
}, CONFIG.delay);
});
}
// 使用 MutationObserver 监听动态内容
function observeDOMChanges() {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
// 只处理元素节点
if (node.nodeType === 1) {
// 检查节点本身是否是链接
if (node.tagName === 'A') {
processLink(node);
}
// 检查节点下的所有链接
node.querySelectorAll('a').forEach(processLink);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 初始执行 + 启动监听
document.querySelectorAll('a').forEach(processLink); // 处理页面已有的链接
observeDOMChanges(); // 监听后续动态添加的链接
})();