TorrentBD: Batch Torrent Downloader

Download torrents from multiple pages at once

// ==UserScript==
// @name          TorrentBD: Batch Torrent Downloader
// @namespace     eLibrarian-userscripts
// @description   Download torrents from multiple pages at once
// @version       0.1
// @author        gaara
// @license       GPL-3.0-or-later
// @match         https://*.torrentbd.net/*
// @match         https://*.torrentbd.com/*
// @match         https://*.torrentbd.org/*
// @match         https://*.torrentbd.me/*
// @grant         none
// @run-at        document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Only run on download history page
    if (!window.location.pathname.includes('download-history.php')) return;

    const state = {
        isRunning: false,
        shouldStop: false
    };

    const config = {
        delayBetweenPages: 500,    // Delay between loading pages (ms)
        delayBetweenDownloads: 100  // Delay between downloading torrents (ms)
    };

    // Add the download button
    function addDownloadButton() {
        const pagination = document.querySelector('.pagination-block');
        if (!pagination || document.getElementById('batch-download-btn')) return;

        const button = document.createElement('button');
        button.id = 'batch-download-btn';
        button.textContent = 'Download All Pages';
        button.style.cssText = `
            margin: 15px auto;
            display: block;
            padding: 8px 16px;
            background-color: #4b8b61;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s;
        `;

        button.addEventListener('click', () => {
            if (state.isRunning) {
                state.shouldStop = true;
                updateButtonState(false);
            } else {
                showSettingsDialog();
            }
        });
        pagination.appendChild(button);
    }

    // Update button appearance
    function updateButtonState(isRunning) {
        const button = document.getElementById('batch-download-btn');
        if (!button) return;

        state.isRunning = isRunning;
        button.textContent = isRunning ? 'Stop Download' : 'Download All Pages';
        button.style.backgroundColor = isRunning ? '#ef5350' : '#4b8b61';
    }

    // Show settings dialog
    function showSettingsDialog() {
        const totalPages = detectTotalPages();
        if (!totalPages) {
            alert('Could not detect total pages');
            return;
        }

        const dialog = document.createElement('div');
        dialog.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        `;

        const content = document.createElement('div');
        content.style.cssText = `
            width: 400px;
            background: #2d2d2d;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.5);
        `;

        content.innerHTML = `
            <h3 style="color: #fff; margin: 0 0 20px; text-align: center;">Download Settings</h3>
            
            <div style="margin-bottom: 15px;">
                <label style="display: block; color: #fff; margin-bottom: 5px;">Start Page:</label>
                <input type="number" id="start-page" min="1" max="${totalPages}" value="1" style="
                    width: 100%;
                    padding: 8px;
                    background: #3d3d3d;
                    border: 1px solid #555;
                    border-radius: 4px;
                    color: #fff;
                    box-sizing: border-box;
                ">
            </div>

            <div style="margin-bottom: 15px;">
                <label style="display: block; color: #fff; margin-bottom: 5px;">End Page:</label>
                <input type="number" id="end-page" min="1" max="${totalPages}" value="${totalPages}" style="
                    width: 100%;
                    padding: 8px;
                    background: #3d3d3d;
                    border: 1px solid #555;
                    border-radius: 4px;
                    color: #fff;
                    box-sizing: border-box;
                ">
            </div>

            <div style="margin-bottom: 15px;">
                <label style="display: block; color: #fff; margin-bottom: 5px;">Delay between pages (ms):</label>
                <input type="number" id="page-delay" min="100" max="5000" value="${config.delayBetweenPages}" style="
                    width: 100%;
                    padding: 8px;
                    background: #3d3d3d;
                    border: 1px solid #555;
                    border-radius: 4px;
                    color: #fff;
                    box-sizing: border-box;
                ">
            </div>

            <div style="margin-bottom: 20px;">
                <label style="display: block; color: #fff; margin-bottom: 5px;">Delay between downloads (ms):</label>
                <input type="number" id="download-delay" min="50" max="2000" value="${config.delayBetweenDownloads}" style="
                    width: 100%;
                    padding: 8px;
                    background: #3d3d3d;
                    border: 1px solid #555;
                    border-radius: 4px;
                    color: #fff;
                    box-sizing: border-box;
                ">
            </div>

            <div style="display: flex; justify-content: space-between;">
                <button id="cancel-btn" style="
                    padding: 8px 16px;
                    background: #555;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                ">Cancel</button>
                <button id="start-btn" style="
                    padding: 8px 16px;
                    background: #4b8b61;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                ">Start Download</button>
            </div>
        `;

        dialog.appendChild(content);
        document.body.appendChild(dialog);

        // Handle dialog buttons
        document.getElementById('cancel-btn').onclick = () => {
            document.body.removeChild(dialog);
        };

        document.getElementById('start-btn').onclick = () => {
            const startPage = parseInt(document.getElementById('start-page').value);
            const endPage = parseInt(document.getElementById('end-page').value);
            const pageDelay = parseInt(document.getElementById('page-delay').value);
            const downloadDelay = parseInt(document.getElementById('download-delay').value);

            if (startPage > endPage) {
                alert('Start page cannot be greater than end page');
                return;
            }

            config.delayBetweenPages = pageDelay;
            config.delayBetweenDownloads = downloadDelay;
            document.body.removeChild(dialog);
            startDownload(startPage, endPage);
        };
    }

    // Show a progress notification
    function showToast(message, showProgress = false) {
        let toast = document.getElementById('download-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'download-toast';
            toast.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: rgba(50, 50, 50, 0.95);
                color: white;
                padding: 15px;
                border-radius: 6px;
                z-index: 10000;
                text-align: center;
            `;
            document.body.appendChild(toast);
        }

        if (showProgress) {
            let progress = toast.querySelector('.progress');
            if (!progress) {
                progress = document.createElement('div');
                progress.className = 'progress';
                progress.style.cssText = `
                    width: 100%;
                    height: 4px;
                    background: #555;
                    margin-top: 10px;
                    border-radius: 2px;
                    overflow: hidden;
                `;
                const bar = document.createElement('div');
                bar.className = 'progress-bar';
                bar.style.cssText = `
                    width: 0%;
                    height: 100%;
                    background: #4b8b61;
                    transition: width 0.3s;
                `;
                progress.appendChild(bar);
                toast.appendChild(progress);
            }
        }

        toast.innerHTML = message;
        if (showProgress) {
            const progress = document.createElement('div');
            progress.className = 'progress';
            progress.style.cssText = `
                width: 100%;
                height: 4px;
                background: #555;
                margin-top: 10px;
                border-radius: 2px;
                overflow: hidden;
            `;
            const bar = document.createElement('div');
            bar.className = 'progress-bar';
            bar.style.cssText = `
                width: 0%;
                height: 100%;
                background: #4b8b61;
                transition: width 0.3s;
            `;
            progress.appendChild(bar);
            toast.appendChild(progress);
        }
    }

    function updateProgress(percent) {
        const bar = document.querySelector('#download-toast .progress-bar');
        if (bar) bar.style.width = percent + '%';
    }

    // Modify startDownload to accept parameters
    async function startDownload(startPage, endPage) {
        state.isRunning = true;
        state.shouldStop = false;
        updateButtonState(true);
        const downloadedFiles = [];

        showToast('Starting download...', true);
        
        for (let currentPage = startPage; currentPage <= endPage; currentPage++) {
            if (state.shouldStop) {
                if (downloadedFiles.length > 0) {
                    showToast('Creating zip of downloaded torrents...', true);
                    await createZip(downloadedFiles);
                    showToast(`Stopped. Downloaded ${downloadedFiles.length} torrents.`);
                } else {
                    showToast('Download stopped. No torrents were downloaded.');
                }
                setTimeout(() => {
                    const toast = document.getElementById('download-toast');
                    if (toast) toast.remove();
                }, 3000);
                updateButtonState(false);
                return;
            }

            // Update overall progress
            const pageProgress = Math.round(((currentPage - startPage) / (endPage - startPage)) * 100);
            updateProgress(pageProgress);
            showToast(`Processing page ${currentPage}/${endPage} (${pageProgress}%)`, true);

            // Get torrents from the current page
            const torrents = await getTorrentsFromPage(currentPage);
            
            // Download each torrent
            for (let i = 0; i < torrents.length; i++) {
                if (state.shouldStop) {
                    if (downloadedFiles.length > 0) {
                        showToast('Creating zip of downloaded torrents...', true);
                        await createZip(downloadedFiles);
                        showToast(`Stopped. Downloaded ${downloadedFiles.length} torrents.`);
                    } else {
                        showToast('Download stopped. No torrents were downloaded.');
                    }
                    setTimeout(() => {
                        const toast = document.getElementById('download-toast');
                        if (toast) toast.remove();
                    }, 3000);
                    updateButtonState(false);
                    return;
                }

                const torrent = torrents[i];
                try {
                    const file = await downloadTorrent(torrent);
                    if (file) downloadedFiles.push(file);
                    
                    // Update progress including current torrent
                    const totalProgress = Math.round((((currentPage - startPage) * 100 + (i + 1) * 100 / torrents.length) / (endPage - startPage + 1)));
                    updateProgress(totalProgress);
                    showToast(`Page ${currentPage}/${endPage}: Downloading ${i + 1}/${torrents.length}`, true);
                    
                    await new Promise(r => setTimeout(r, config.delayBetweenDownloads));
                } catch (error) {
                    console.error('Error downloading torrent:', error);
                }
            }

            if (currentPage < endPage) {
                await new Promise(r => setTimeout(r, config.delayBetweenPages));
            }
        }

        // Create zip of all downloaded files
        if (downloadedFiles.length > 0) {
            showToast('Creating zip file...', true);
            await createZip(downloadedFiles);
        }

        showToast(`Completed! Downloaded ${downloadedFiles.length} torrents.`);
        setTimeout(() => {
            const toast = document.getElementById('download-toast');
            if (toast) toast.remove();
        }, 3000);

        state.isRunning = false;
        updateButtonState(false);
    }

    // Detect total number of pages
    function detectTotalPages() {
        const paginationLinks = document.querySelectorAll('.pagination li a');
        let highestPage = 1;

        paginationLinks.forEach(link => {
            const match = link.href.match(/page=(\d+)/);
            if (match) {
                const page = parseInt(match[1], 10);
                if (page > highestPage) highestPage = page;
            }
        });

        return highestPage;
    }

    // Get torrents from a specific page
    async function getTorrentsFromPage(page) {
        const torrents = [];
        
        // If we're already on the right page
        if ((page === 1 && !window.location.href.includes('page=')) || 
            window.location.href.includes(`page=${page}`)) {
            document.querySelectorAll('table.notif-table tbody tr').forEach(row => {
                const link = row.querySelector('td.dl-btn-td a');
                if (link) torrents.push(link.href);
            });
        } else {
            // Load the page content
            const url = new URL(window.location.href);
            url.searchParams.set('page', page);
            
            try {
                const response = await fetch(url, { credentials: 'include' });
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                
                doc.querySelectorAll('table.notif-table tbody tr').forEach(row => {
                    const link = row.querySelector('td.dl-btn-td a');
                    if (link) torrents.push(link.href);
                });
            } catch (error) {
                console.error(`Error loading page ${page}:`, error);
            }
        }
        
        return torrents;
    }

    // Download a single torrent
    async function downloadTorrent(url) {
        try {
            const response = await fetch(url, { credentials: 'include' });
            if (!response.ok) throw new Error(`Failed to download: ${response.statusText}`);

            const blob = await response.blob();
            const disposition = response.headers.get('Content-Disposition');
            const matches = disposition?.match(/filename[^;=\n]*=([^;\n]*)/);
            const filename = matches ? matches[1].trim().replace(/['"]/g, '') : `torrent_${Date.now()}.torrent`;

            return { blob, filename };
        } catch (error) {
            console.error('Download error:', error);
            return null;
        }
    }

    // Create a zip file from downloaded torrents
    async function createZip(files) {
        // Load JSZip if not already loaded
        if (typeof JSZip === 'undefined') {
            await new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.0/jszip.min.js';
                script.onload = resolve;
                script.onerror = reject;
                document.head.appendChild(script);
            });
        }

        const zip = new JSZip();
        files.forEach(file => zip.file(file.filename, file.blob));

        const content = await zip.generateAsync({ type: 'blob' });
        const date = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
        
        const a = document.createElement('a');
        a.href = URL.createObjectURL(content);
        a.download = `TorrentBD_Batch_${date}.zip`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(a.href);
    }

    // Initialize
    addDownloadButton();
})();

QingJ © 2025

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