您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhance Hugging Face model downloads with batch selection and custom naming
// ==UserScript== // @name Hugging Face Batch Downloader // @namespace http://tampermonkey.net/ // @version 1.6.1 // @description Enhance Hugging Face model downloads with batch selection and custom naming // @author Xunjian Yin // @match https://huggingface.co/*/tree/* // @match https://huggingface.co/*/blob/* // @match https://hf-mirror.com/*/tree/* // @match https://hf-mirror.com/*/blob/* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // Wait for the page to fully load function waitForElement(selector, timeout = 10000) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(() => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); }); } // Extract model path from URL (organization/model-name) function getModelPath() { const urlParts = window.location.pathname.split('/'); if (urlParts.length >= 3) { // Return the organization/model-name format for folder structure return `${urlParts[1]}/${urlParts[2]}`; } return 'huggingface-model'; } // Get just the model name for logging purposes function getModelName() { const urlParts = window.location.pathname.split('/'); if (urlParts.length >= 3) { return `${urlParts[1]}/${urlParts[2]}`; } return 'huggingface-model'; } // Create download link with organized filename (browser-compatible) function createDownloadLink(originalUrl, filename, modelPath) { // Ensure we're using the resolve URL for downloads, not blob URL let downloadUrl = originalUrl; if (downloadUrl.includes('/blob/')) { downloadUrl = downloadUrl.replace('/blob/', '/resolve/'); } // Force download by adding download parameter if it's not already there if (!downloadUrl.includes('download=true')) { const separator = downloadUrl.includes('?') ? '&' : '?'; downloadUrl += `${separator}download=true`; } // Create organized filename since browsers don't create folders via download attribute // Format: [Org-Model] filename.ext const modelNameForFile = modelPath.replace('/', '-'); const downloadFilename = `[${modelNameForFile}] ${filename}`; console.log(`HF Batch Downloader: Downloading ${downloadFilename} from ${downloadUrl}`); // Create download link with proper attributes const link = document.createElement('a'); link.href = downloadUrl; link.download = downloadFilename; link.target = '_blank'; // Fallback in case download fails link.rel = 'noopener noreferrer'; link.style.display = 'none'; // Add to DOM, click, and remove document.body.appendChild(link); try { link.click(); } catch (error) { console.error(`HF Batch Downloader: Failed to trigger download for ${filename}:`, error); // Fallback: open in new tab window.open(downloadUrl, '_blank'); } // Clean up after a short delay setTimeout(() => { if (link.parentNode) { document.body.removeChild(link); } }, 100); } // Add batch download functionality async function addBatchDownloadUI() { console.log('HF Batch Downloader: Looking for file list...'); // Try multiple selectors to find the file list let fileContainer = null; const selectors = [ '[data-target="TreeView"]', '.file-list', '[class*="file"]', 'main', 'article', '.container' ]; for (const selector of selectors) { fileContainer = await waitForElement(selector, 3000); if (fileContainer) { console.log(`HF Batch Downloader: Found container with selector: ${selector}`); break; } } if (!fileContainer) { console.log('HF Batch Downloader: No suitable container found, trying body'); fileContainer = document.body; } const modelName = getModelName(); const modelPath = getModelPath(); console.log(`HF Batch Downloader: Model path extracted: ${modelPath}`); // Find all download links const allLinks = document.querySelectorAll('a[href*="/resolve/"]'); console.log(`HF Batch Downloader: Found ${allLinks.length} potential download links`); if (allLinks.length === 0) { console.log('HF Batch Downloader: No download links found'); return; } // Create control panel with HF-like styling const controlPanel = document.createElement('div'); controlPanel.id = 'hf-batch-downloader-panel'; controlPanel.style.cssText = ` background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px 16px; margin: 16px 0 8px 0; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); `; controlPanel.innerHTML = ` <div style="display: flex; align-items: center; gap: 8px; color: #475569; font-weight: 500;"> <span style="font-size: 16px;">📦</span> <span>Batch Download</span> </div> <button id="hf-selectAll" style=" padding: 6px 12px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; ">Select All</button> <button id="hf-selectNone" style=" padding: 6px 12px; background: #6b7280; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; ">Select None</button> <button id="hf-downloadSelected" style=" padding: 6px 12px; background: #10b981; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; ">Download Selected</button> <span id="hf-selectedCount" style="color: #6b7280; margin-left: 4px; font-size: 13px;">0 files selected</span> `; // Find the best insertion point - look for file list container let insertionPoint = null; // Try to find the files section specifically const filesSection = document.querySelector('[data-testid="files-section"]') || document.querySelector('.files') || document.querySelector('[class*="file"]') || document.querySelector('main section'); if (filesSection) { insertionPoint = filesSection; } else { // Fallback to main content area const mainContent = document.querySelector('main') || document.querySelector('.container'); if (mainContent) { insertionPoint = mainContent; } } if (insertionPoint) { // Insert at the beginning of the files section, not the very top of main const firstChild = insertionPoint.firstChild; if (firstChild) { insertionPoint.insertBefore(controlPanel, firstChild); } else { insertionPoint.appendChild(controlPanel); } } else { // Last resort - insert after the header const header = document.querySelector('header') || document.querySelector('nav'); if (header && header.nextSibling) { header.parentNode.insertBefore(controlPanel, header.nextSibling); } else { document.body.insertBefore(controlPanel, document.body.firstChild); } } // Process each download link const fileList = []; allLinks.forEach((link, index) => { const href = link.href; // Accept both /resolve/ and /blob/ links, we'll convert blob to resolve later const isDownloadLink = href.includes('/resolve/') || href.includes('/blob/'); const isNotFolderLink = !href.includes('/tree/'); if (!isDownloadLink || !isNotFolderLink) { return; } // Get the filename from the link text or URL let fileName = link.textContent.trim(); if (!fileName || fileName === '') { const urlParts = href.split('/'); fileName = urlParts[urlParts.length - 1] || `file-${index}`; } // Skip if filename is empty or looks like a folder if (!fileName || fileName === '..') { return; } console.log(`HF Batch Downloader: Processing file: ${fileName}`); // Create a subtle, clean checkbox that integrates naturally const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `hf-file-checkbox-${index}`; checkbox.style.cssText = ` width: 14px; height: 14px; margin: 0 6px 0 0; cursor: pointer; accent-color: #656d76; opacity: 0.7; transition: opacity 0.2s ease; vertical-align: middle; position: relative; top: -1px; `; // Add hover effect to make it more visible on interaction checkbox.addEventListener('mouseenter', () => { checkbox.style.opacity = '1'; checkbox.style.accentColor = '#0969da'; }); checkbox.addEventListener('mouseleave', () => { if (!checkbox.checked) { checkbox.style.opacity = '0.7'; checkbox.style.accentColor = '#656d76'; } }); checkbox.addEventListener('change', () => { if (checkbox.checked) { checkbox.style.opacity = '1'; checkbox.style.accentColor = '#0969da'; } else { checkbox.style.opacity = '0.7'; checkbox.style.accentColor = '#656d76'; } }); // Find the file icon (first element in the row that's likely the icon) const fileIcon = link.parentElement.querySelector('svg') || link.parentElement.querySelector('[class*="icon"]') || link.parentElement.querySelector('img'); if (fileIcon) { // Insert checkbox right before the file icon for clean alignment fileIcon.parentElement.insertBefore(checkbox, fileIcon); } else { // Fallback: find the first element that looks like a file row start let insertionPoint = link; // Walk up to find a container, but not too far let currentElement = link.parentElement; let attempts = 0; while (currentElement && attempts < 3) { if (currentElement.children.length > 1) { insertionPoint = currentElement.firstChild; break; } currentElement = currentElement.parentElement; attempts++; } insertionPoint.parentElement.insertBefore(checkbox, insertionPoint); } fileList.push({ checkbox, fileName, downloadUrl: href, element: link.parentElement }); checkbox.addEventListener('change', updateSelectedCount); }); console.log(`HF Batch Downloader: Processed ${fileList.length} downloadable files`); // Control panel event handlers function updateSelectedCount() { const selectedCheckboxes = fileList.filter(file => file.checkbox.checked); const countElement = document.getElementById('hf-selectedCount'); if (countElement) { countElement.textContent = `${selectedCheckboxes.length} files selected`; } } document.getElementById('hf-selectAll').addEventListener('click', () => { fileList.forEach(file => file.checkbox.checked = true); updateSelectedCount(); }); document.getElementById('hf-selectNone').addEventListener('click', () => { fileList.forEach(file => file.checkbox.checked = false); updateSelectedCount(); }); document.getElementById('hf-downloadSelected').addEventListener('click', async () => { const selectedFiles = fileList.filter(file => file.checkbox.checked); if (selectedFiles.length === 0) { alert('Please select at least one file to download.'); return; } const downloadBtn = document.getElementById('hf-downloadSelected'); const originalText = downloadBtn.textContent; downloadBtn.textContent = `Downloading ${selectedFiles.length} files...`; downloadBtn.disabled = true; console.log(`HF Batch Downloader: Starting download of ${selectedFiles.length} files`); let successCount = 0; let errorCount = 0; // Download files with delay to avoid overwhelming the browser for (let i = 0; i < selectedFiles.length; i++) { const file = selectedFiles[i]; console.log(`HF Batch Downloader: Downloading ${i + 1}/${selectedFiles.length}: ${file.fileName}`); try { createDownloadLink(file.downloadUrl, file.fileName, modelPath); successCount++; // Update button text with progress downloadBtn.textContent = `Downloading ${i + 1}/${selectedFiles.length}...`; } catch (error) { console.error(`HF Batch Downloader: Error downloading ${file.fileName}:`, error); errorCount++; } // Add delay between downloads (longer delay for more reliability) if (i < selectedFiles.length - 1) { await new Promise(resolve => setTimeout(resolve, 1200)); } } downloadBtn.textContent = originalText; downloadBtn.disabled = false; console.log(`HF Batch Downloader: Completed. Success: ${successCount}, Errors: ${errorCount}`); // Show appropriate completion message if (errorCount === 0) { alert(`✅ Successfully started downloading all ${selectedFiles.length} files! Check your downloads folder.`); } else if (successCount > 0) { alert(`⚠️ Started downloading ${successCount} files successfully, ${errorCount} failed. Check browser console for details and your downloads folder.`); } else { alert(`❌ Failed to download any files. Please check the browser console for error details and try again.`); } }); // Add hover effects for buttons const buttons = controlPanel.querySelectorAll('button'); buttons.forEach(button => { const originalBackground = button.style.background; button.addEventListener('mouseenter', () => { if (button.id === 'hf-selectAll') { button.style.background = '#2563eb'; } else if (button.id === 'hf-selectNone') { button.style.background = '#4b5563'; } else if (button.id === 'hf-downloadSelected') { button.style.background = '#059669'; } button.style.transform = 'translateY(-1px)'; button.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)'; }); button.addEventListener('mouseleave', () => { button.style.background = originalBackground; button.style.transform = 'translateY(0)'; button.style.boxShadow = 'none'; }); button.addEventListener('active', () => { button.style.transform = 'translateY(0)'; }); }); console.log(`HF Batch Downloader: Enhanced ${fileList.length} files for model: ${modelName}`); } // Enhanced individual download links function enhanceIndividualDownloads() { const modelPath = getModelPath(); // Find and enhance existing download links const downloadLinks = document.querySelectorAll('a[href*="/resolve/"]'); downloadLinks.forEach(link => { if (link.dataset.enhanced) return; // Skip already enhanced links const fileName = link.textContent.trim() || link.href.split('/').pop(); link.addEventListener('click', (e) => { e.preventDefault(); createDownloadLink(link.href, fileName, modelPath); }); link.dataset.enhanced = 'true'; const modelNameForFile = modelPath.replace('/', '-'); link.title = `Download as: [${modelNameForFile}] ${fileName}`; }); } // Show a temporary notification function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 10px 15px; border-radius: 5px; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; color: white; background: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'}; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: opacity 0.3s; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => notification.remove(), 300); }, 3000); } // Initialize the script async function init() { console.log('HF Batch Downloader: Initializing v1.6.1...'); // Check if we're on the right page if (!window.location.pathname.includes('/tree/') && !window.location.pathname.includes('/blob/')) { console.log('HF Batch Downloader: Not on a file page, skipping'); return; } // Show loading notification showNotification('🚀 HF Batch Downloader loading...', 'info'); try { // Add batch download UI await addBatchDownloadUI(); // Enhance individual downloads enhanceIndividualDownloads(); showNotification('✅ HF Batch Downloader ready!', 'success'); // Watch for dynamically loaded content const observer = new MutationObserver((mutations) => { // Only enhance if new links are added let hasNewLinks = false; mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.querySelector && node.querySelector('a[href*="/resolve/"]')) { hasNewLinks = true; } } }); } }); if (hasNewLinks) { console.log('HF Batch Downloader: New content detected, re-enhancing...'); enhanceIndividualDownloads(); } }); observer.observe(document.body, { childList: true, subtree: true }); } catch (error) { console.error('HF Batch Downloader: Error during initialization:', error); showNotification('❌ HF Batch Downloader failed to load', 'error'); } } // Start the script when the page is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();