您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==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();
})();