Greasy Fork

Greasy Fork is available in English.

HuggingFace镜像链接提取器

在HuggingFace页面提取下载链接,同时显示原始链接和hf-mirror.com镜像链接。v1.2: 添加文件大小显示、排序功能、优化界面布局

当前为 2025-07-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         HuggingFace镜像链接提取器
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  在HuggingFace页面提取下载链接,同时显示原始链接和hf-mirror.com镜像链接。v1.2: 添加文件大小显示、排序功能、优化界面布局
// @author       AI Assistant
// @match        https://huggingface.co/*
// @match        https://hf-mirror.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=huggingface.co
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 创建样式
    const style = document.createElement('style');
    style.textContent = `
        .hf-extractor-btn {
            position: fixed;
            top: 20px;
            left: 20px;
            width: 60px;
            height: 60px;
            background: linear-gradient(45deg, #ff6b6b, #feca57);
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            font-size: 12px;
            font-weight: bold;
            z-index: 10000;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            transition: all 0.3s ease;
        }
        .hf-extractor-btn:hover {
            transform: scale(1.1);
            box-shadow: 0 6px 20px rgba(0,0,0,0.4);
        }
        .hf-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 9999;
            display: none;
            justify-content: center;
            align-items: center;
        }
        .hf-modal-content {
            background: white;
            border-radius: 15px;
            padding: 20px;
            max-width: 95vw;
            max-height: 95vh;
            overflow: hidden;
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
            min-width: 800px;
            display: flex;
            flex-direction: column;
        }
        .hf-header {
            text-align: center;
            margin-bottom: 15px;
            color: #333;
            border-bottom: 2px solid #eee;
            padding-bottom: 10px;
        }
        .hf-stats {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 10px;
            border-radius: 8px;
            text-align: center;
            margin-bottom: 15px;
            font-weight: bold;
            font-size: 14px;
        }
        .hf-buttons {
            display: flex;
            gap: 8px;
            margin-bottom: 15px;
            flex-wrap: wrap;
        }
        .hf-sort-buttons {
            display: flex;
            gap: 5px;
            margin-bottom: 10px;
            justify-content: center;
        }
        .hf-sort-btn {
            padding: 5px 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            cursor: pointer;
            font-size: 12px;
            background: white;
            transition: all 0.2s ease;
        }
        .hf-sort-btn:hover {
            background: #f0f0f0;
        }
        .hf-sort-btn.active {
            background: #3498db;
            color: white;
            border-color: #3498db;
        }
        .hf-links-wrapper {
            flex: 1;
            overflow-y: auto;
            max-height: calc(95vh - 300px);
        }
        .hf-btn {
            padding: 8px 12px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s ease;
            flex: 1;
            min-width: 100px;
            font-size: 13px;
        }
        .hf-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
        }
        .hf-btn-close { background: #e74c3c; color: white; }
        .hf-btn-copy-first { background: #3498db; color: white; }
        .hf-btn-copy-all-orig { background: #27ae60; color: white; }
        .hf-btn-copy-all-mirror { background: #f39c12; color: white; }
        .hf-link-item {
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 6px;
            padding: 8px;
            margin-bottom: 6px;
            transition: all 0.2s ease;
        }
        .hf-link-item.main-file {
            border: 2px solid #ff6b6b;
            background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
        }
        .hf-link-item:hover {
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            transform: translateY(-1px);
        }
        .hf-file-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 4px;
        }
        .hf-file-name {
            font-weight: bold;
            color: #2c3e50;
            font-size: 14px;
            display: flex;
            align-items: center;
            gap: 6px;
            flex: 1;
        }
        .hf-file-size {
            font-size: 12px;
            color: #666;
            background: #e9ecef;
            padding: 2px 6px;
            border-radius: 4px;
            margin-left: 8px;
        }
        .hf-main-file {
            background: linear-gradient(45deg, #ff6b6b, #feca57);
            color: white;
            padding: 1px 6px;
            border-radius: 8px;
            font-size: 10px;
            font-weight: bold;
        }
        .hf-link-row {
            display: flex;
            align-items: center;
            margin-bottom: 2px;
            padding: 4px;
            background: white;
            border-radius: 4px;
        }
        .hf-link-label {
            font-weight: bold;
            min-width: 45px;
            margin-right: 6px;
            font-size: 11px;
        }
        .hf-link-url {
            flex: 1;
            font-family: monospace;
            font-size: 11px;
            word-break: break-all;
            margin-right: 6px;
        }
        .hf-copy-btn {
            padding: 3px 8px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 10px;
            font-weight: bold;
            transition: all 0.2s ease;
            min-width: 40px;
        }
        .hf-copy-orig { background: #3498db; color: white; }
        .hf-copy-mirror { background: #f39c12; color: white; }
        .hf-copy-btn:hover { opacity: 0.8; }
        .hf-more-info {
            text-align: center;
            padding: 15px;
            color: #7f8c8d;
            font-style: italic;
            background: #ecf0f1;
            border-radius: 8px;
        }
    `;
    document.head.appendChild(style);

    // 创建提取按钮
    const extractBtn = document.createElement('button');
    extractBtn.className = 'hf-extractor-btn';
    extractBtn.innerHTML = '🔗<br>提取';
    document.body.appendChild(extractBtn);

    // 创建模态框
    const modal = document.createElement('div');
    modal.className = 'hf-modal';
    modal.innerHTML = `
        <div class="hf-modal-content">
            <div class="hf-header">
                <h2>🚀 HuggingFace 下载链接提取器</h2>
                <p>同时提供原始链接和镜像链接</p>
            </div>
            <div class="hf-stats" id="hf-stats"></div>
            <div class="hf-buttons">
                <button class="hf-btn hf-btn-close" id="hf-close">❌ 关闭</button>
                <button class="hf-btn hf-btn-copy-first" id="hf-copy-first">📋 复制第一个</button>
                <button class="hf-btn hf-btn-copy-all-orig" id="hf-copy-all-orig">📄 复制全部原始</button>
                <button class="hf-btn hf-btn-copy-all-mirror" id="hf-copy-all-mirror">🚀 复制全部镜像</button>
            </div>
            <div class="hf-sort-buttons">
                <button class="hf-sort-btn active" data-sort="default">🏷️ 默认排序</button>
                <button class="hf-sort-btn" data-sort="name">📝 按名称</button>
                <button class="hf-sort-btn" data-sort="size">📊 按大小</button>
                <button class="hf-sort-btn" data-sort="type">📁 按类型</button>
            </div>
            <div class="hf-links-wrapper">
                <div id="hf-links-container"></div>
            </div>
        </div>
    `;
    document.body.appendChild(modal);

    // 清理文件名,移除查询参数
    function cleanFileName(fileName) {
        return fileName.replace(/\?.*$/, '');
    }

    // 判断是否为主要文件
    function isMainFile(fileName) {
        const mainFilePatterns = [
            /^README\.md$/i,
            /^config\.json$/i,
            /^model\.safetensors$/i,
            /^pytorch_model\.bin$/i,
            /^model\.onnx$/i,
            /^tokenizer\.json$/i,
            /^tokenizer_config\.json$/i,
            /^vocab\.txt$/i,
            /^merges\.txt$/i,
            /\.py$/i,
            /^requirements\.txt$/i,
            /^setup\.py$/i,
            /^__init__\.py$/i,
            /^Dockerfile$/i,
            /^\.dockerignore$/i
        ];

        return mainFilePatterns.some(pattern => pattern.test(fileName));
    }

    // 格式化文件大小
    function formatFileSize(bytes) {
        if (!bytes || bytes === 0) return '未知';
        const sizes = ['B', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
    }

    // 从元素中提取文件大小
    function extractFileSize(element) {
        let sizeText = '';

        const parentRow = element.closest('tr') || element.closest('.file-item') || element.closest('[class*="file"]');
        if (parentRow) {
            const sizeElements = parentRow.querySelectorAll('[class*="size"], [class*="Size"], .text-sm, .text-xs');
            for (const sizeEl of sizeElements) {
                const text = sizeEl.textContent.trim();
                if (/\d+(\.\d+)?\s*(B|KB|MB|GB|bytes)/i.test(text)) {
                    sizeText = text;
                    break;
                }
            }
        }

        if (!sizeText) {
            sizeText = element.getAttribute('data-size') ||
                      element.getAttribute('data-file-size') ||
                      element.closest('[data-size]')?.getAttribute('data-size') || '';
        }

        return sizeText || '未知';
    }

    // 解析文件大小为字节数(用于排序)
    function parseSizeToBytes(sizeStr) {
        if (!sizeStr || sizeStr === '未知') return 0;

        const match = sizeStr.match(/(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|bytes)/i);
        if (!match) return 0;

        const value = parseFloat(match[1]);
        const unit = match[2].toUpperCase();

        const multipliers = {
            'B': 1,
            'BYTES': 1,
            'KB': 1024,
            'MB': 1024 * 1024,
            'GB': 1024 * 1024 * 1024
        };

        return value * (multipliers[unit] || 1);
    }

    // 全局变量存储当前排序方式
    let currentSortType = 'default';
    let allLinks = [];

    // 提取链接函数
    function extractLinks() {
        const links = [];
        const elements = document.querySelectorAll('a[download][href]');

        elements.forEach(element => {
            const href = element.getAttribute('href');
            if (href) {
                const originalLink = href.startsWith('http') ? href : 'https://huggingface.co' + href;
                const mirrorLink = originalLink.replace('huggingface.co', 'hf-mirror.com');
                const rawFileName = href.split('/').pop() || 'unknown';
                const fileName = cleanFileName(rawFileName);
                const isMain = isMainFile(fileName);
                const fileSize = extractFileSize(element);
                const fileSizeBytes = parseSizeToBytes(fileSize);

                links.push({
                    original: originalLink,
                    mirror: mirrorLink,
                    fileName: fileName,
                    isMainFile: isMain,
                    fileSize: fileSize,
                    fileSizeBytes: fileSizeBytes
                });
            }
        });

        allLinks = links;
        return sortLinks(links, 'default');
    }

    // 排序函数
    function sortLinks(links, sortType) {
        const sorted = [...links];

        switch (sortType) {
            case 'name':
                return sorted.sort((a, b) => a.fileName.localeCompare(b.fileName));
            case 'size':
                return sorted.sort((a, b) => b.fileSizeBytes - a.fileSizeBytes);
            case 'type':
                return sorted.sort((a, b) => {
                    const extA = a.fileName.split('.').pop().toLowerCase();
                    const extB = b.fileName.split('.').pop().toLowerCase();
                    return extA.localeCompare(extB);
                });
            case 'default':
            default:
                return sorted.sort((a, b) => {
                    if (a.isMainFile && !b.isMainFile) return -1;
                    if (!a.isMainFile && b.isMainFile) return 1;
                    return a.fileName.localeCompare(b.fileName);
                });
        }
    }

    // 复制到剪贴板
    function copyToClipboard(text, button) {
        navigator.clipboard.writeText(text).then(() => {
            const originalText = button.textContent;
            button.textContent = '✅ 已复制!';
            button.style.background = '#27ae60';
            setTimeout(() => {
                button.textContent = originalText;
                button.style.background = '';
            }, 1500);
        }).catch(err => {
            console.error('复制失败:', err);
            alert('复制失败,请手动复制');
        });
    }

    // 显示链接
    function displayLinks(links) {
        const container = document.getElementById('hf-links-container');
        const stats = document.getElementById('hf-stats');

        const mainFileCount = links.filter(link => link.isMainFile).length;
        const totalSize = links.reduce((sum, link) => sum + link.fileSizeBytes, 0);
        const totalSizeStr = formatFileSize(totalSize);

        const statsText = mainFileCount > 0
            ? `📊 共 <strong>${links.length}</strong> 个文件,其中 <strong>${mainFileCount}</strong> 个主要文件,总大小: <strong>${totalSizeStr}</strong>`
            : `📊 共 <strong>${links.length}</strong> 个文件,总大小: <strong>${totalSizeStr}</strong>`;

        stats.innerHTML = statsText;

        if (links.length === 0) {
            container.innerHTML = '<div class="hf-more-info">❌ 未找到任何下载链接</div>';
            return;
        }

        let html = '';

        for (let i = 0; i < links.length; i++) {
            const link = links[i];
            const mainFileClass = link.isMainFile ? ' main-file' : '';
            const fileIcon = link.isMainFile ? '⭐' : '📁';
            const mainFileTag = link.isMainFile ? '<span class="hf-main-file">主要</span>' : '';

            html += `
                <div class="hf-link-item${mainFileClass}">
                    <div class="hf-file-header">
                        <div class="hf-file-name">
                            ${fileIcon} ${link.fileName}
                            ${mainFileTag}
                        </div>
                        <div class="hf-file-size">${link.fileSize}</div>
                    </div>
                    <div class="hf-link-row">
                        <span class="hf-link-label" style="color: #3498db;">🔗</span>
                        <span class="hf-link-url">${link.original}</span>
                        <button class="hf-copy-btn hf-copy-orig" onclick="copyToClipboard('${link.original}', this)">复制</button>
                    </div>
                    <div class="hf-link-row">
                        <span class="hf-link-label" style="color: #f39c12;">🚀</span>
                        <span class="hf-link-url">${link.mirror}</span>
                        <button class="hf-copy-btn hf-copy-mirror" onclick="copyToClipboard('${link.mirror}', this)">复制</button>
                    </div>
                </div>
            `;
        }

        container.innerHTML = html;
    }

    // 事件监听
    extractBtn.addEventListener('click', () => {
        const links = extractLinks();
        displayLinks(links);
        modal.style.display = 'flex';

        // 更新按钮事件
        document.getElementById('hf-close').onclick = () => {
            modal.style.display = 'none';
        };

        document.getElementById('hf-copy-first').onclick = () => {
            if (allLinks.length > 0) {
                const currentLinks = sortLinks(allLinks, currentSortType);
                copyToClipboard(currentLinks[0].original, document.getElementById('hf-copy-first'));
            }
        };

        document.getElementById('hf-copy-all-orig').onclick = () => {
            const allOriginal = allLinks.map(link => link.original).join('\n');
            copyToClipboard(allOriginal, document.getElementById('hf-copy-all-orig'));
        };

        document.getElementById('hf-copy-all-mirror').onclick = () => {
            const allMirror = allLinks.map(link => link.mirror).join('\n');
            copyToClipboard(allMirror, document.getElementById('hf-copy-all-mirror'));
        };

        // 排序按钮事件
        document.querySelectorAll('.hf-sort-btn').forEach(btn => {
            btn.onclick = () => {
                const sortType = btn.getAttribute('data-sort');
                currentSortType = sortType;

                // 更新按钮状态
                document.querySelectorAll('.hf-sort-btn').forEach(b => b.classList.remove('active'));
                btn.classList.add('active');

                // 重新排序并显示
                const sortedLinks = sortLinks(allLinks, sortType);
                displayLinks(sortedLinks);
            };
        });
    });

    // 点击模态框外部关闭
    modal.addEventListener('click', (e) => {
        if (e.target === modal) {
            modal.style.display = 'none';
        }
    });

    // 全局函数供内联事件使用
    window.copyToClipboard = copyToClipboard;

    console.log('🚀 HuggingFace镜像链接提取器v1.2已加载');
})();