磁力链接悬浮预览

在磁力链接后添加标识符号,通过点击或悬停显示完整链接信息

目前為 2025-08-04 提交的版本,檢視 最新版本

// ==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(); // 监听后续动态添加的链接

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址