main
parent
c166be9e62
commit
92010207b5
@ -0,0 +1,16 @@ |
||||
图片下载后端工具, 前端可用油猴脚本(随意, 只要能发起 post请求即可) |
||||
|
||||
post 数据格式为 |
||||
|
||||
```json |
||||
{ |
||||
"title": "文件夹名称", |
||||
"source": "网站来源, 用于下载保存的文件夹", |
||||
"imgs": { |
||||
"0001": "https://icon-icons.com/images/menu_photos.png", |
||||
"0002": "https://icon-icons.com/images/flags/zh.webp" |
||||
} |
||||
} |
||||
``` |
||||
|
||||
端口: 55830 |
||||
@ -0,0 +1,420 @@ |
||||
// ==UserScript==
|
||||
// @name hd4k_downloader_simple
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 1.2
|
||||
// @description 简单直接的自动翻页图片爬取
|
||||
// @author Your Name
|
||||
// @match *://*/*
|
||||
// @grant GM_xmlhttpRequest
|
||||
// ==/UserScript==
|
||||
|
||||
(function() { |
||||
'use strict'; |
||||
|
||||
const CONFIG = { |
||||
maxPages: 50, |
||||
pageDelay: 1500, |
||||
backendUrl: 'http://127.0.0.1:55830/api/save_json' |
||||
}; |
||||
|
||||
let isCrawling = false; |
||||
let allImages = {}; |
||||
let currentPage = 1; |
||||
let imgIndex = 1; |
||||
let crawledUrls = []; |
||||
const source = 'hd4k'; |
||||
|
||||
const createButton = () => { |
||||
const button = document.createElement('button'); |
||||
button.textContent = '开始爬取'; |
||||
button.id = 'hd4k-btn'; |
||||
|
||||
button.style.position = 'fixed'; |
||||
button.style.top = '14%'; |
||||
button.style.right = '1%'; |
||||
button.style.transform = 'translateY(-50%)'; |
||||
button.style.padding = '8px 16px'; |
||||
button.style.fontSize = '12px'; |
||||
button.style.fontWeight = 'bold'; |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.style.color = '#fff'; |
||||
button.style.border = 'none'; |
||||
button.style.borderRadius = '8px'; |
||||
button.style.cursor = 'pointer'; |
||||
button.style.zIndex = '10000'; |
||||
button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; |
||||
button.style.transition = 'all 0.3s ease'; |
||||
|
||||
button.addEventListener('mouseenter', () => { |
||||
if (!isCrawling) { |
||||
button.style.backgroundColor = '#1a6ee0'; |
||||
button.style.transform = 'translateY(-50%) scale(1.05)'; |
||||
} |
||||
}); |
||||
|
||||
button.addEventListener('mouseleave', () => { |
||||
if (!isCrawling) { |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.style.transform = 'translateY(-50%) scale(1)'; |
||||
} |
||||
}); |
||||
|
||||
button.addEventListener('click', startCrawling); |
||||
|
||||
return button; |
||||
}; |
||||
|
||||
const createStatusDisplay = () => { |
||||
const statusDiv = document.createElement('div'); |
||||
statusDiv.id = 'hd4k-status'; |
||||
statusDiv.style.position = 'fixed'; |
||||
statusDiv.style.top = '18%'; |
||||
statusDiv.style.right = '1%'; |
||||
statusDiv.style.padding = '10px'; |
||||
statusDiv.style.backgroundColor = 'rgba(0,0,0,0.85)'; |
||||
statusDiv.style.color = '#fff'; |
||||
statusDiv.style.borderRadius = '5px'; |
||||
statusDiv.style.fontSize = '12px'; |
||||
statusDiv.style.zIndex = '9999'; |
||||
statusDiv.style.minWidth = '180px'; |
||||
statusDiv.style.display = 'none'; |
||||
|
||||
return statusDiv; |
||||
}; |
||||
|
||||
const updateStatus = (message) => { |
||||
const statusDiv = document.getElementById('hd4k-status'); |
||||
if (statusDiv) { |
||||
statusDiv.innerHTML = message; |
||||
statusDiv.style.display = 'block'; |
||||
} |
||||
console.log(`[状态] ${message}`); |
||||
}; |
||||
|
||||
const getCurrentPageImages = () => { |
||||
const images = document.querySelectorAll('img'); |
||||
const imageUrls = []; |
||||
const seenUrls = new Set(); |
||||
|
||||
images.forEach(img => { |
||||
let src = img.src || img.dataset.src || img.dataset.original || img.currentSrc; |
||||
|
||||
if (src && src.trim() && !src.startsWith('data:') && !src.startsWith('blob:')) { |
||||
let fullUrl = src; |
||||
if (src.startsWith('//')) { |
||||
fullUrl = window.location.protocol + src; |
||||
} else if (src.startsWith('/')) { |
||||
fullUrl = window.location.origin + src; |
||||
} else if (!src.startsWith('http')) { |
||||
fullUrl = new URL(src, window.location.href).href; |
||||
} |
||||
|
||||
const isImage = /\.(jpg|jpeg|png|gif|webp|bmp|tiff)(\?.*)?$/i.test(fullUrl); |
||||
if (isImage && !seenUrls.has(fullUrl)) { |
||||
seenUrls.add(fullUrl); |
||||
imageUrls.push(fullUrl); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
return imageUrls; |
||||
}; |
||||
|
||||
const buildPageUrl = (pageNum) => { |
||||
const currentUrl = window.location.href; |
||||
const htmlIndex = currentUrl.indexOf('html'); |
||||
|
||||
if (htmlIndex === -1) { |
||||
console.error('URL中没有找到html'); |
||||
return currentUrl; |
||||
} |
||||
|
||||
const basePart = currentUrl.substring(0, htmlIndex + 4); |
||||
|
||||
if (pageNum === 1) { |
||||
return basePart; |
||||
} else { |
||||
return basePart + '/' + pageNum; |
||||
} |
||||
}; |
||||
|
||||
const getCurrentPageNumber = () => { |
||||
const currentUrl = window.location.href; |
||||
const htmlIndex = currentUrl.indexOf('html'); |
||||
|
||||
if (htmlIndex === -1) return 1; |
||||
|
||||
const afterHtml = currentUrl.substring(htmlIndex + 4); |
||||
const match = afterHtml.match(/^\/(\d+)/); |
||||
|
||||
if (match) { |
||||
const pageNum = parseInt(match[1], 10); |
||||
if (!isNaN(pageNum) && pageNum > 0) { |
||||
return pageNum; |
||||
} |
||||
} |
||||
|
||||
return 1; |
||||
}; |
||||
|
||||
const sendToBackend = (data) => { |
||||
return new Promise((resolve, reject) => { |
||||
GM_xmlhttpRequest({ |
||||
method: 'POST', |
||||
url: CONFIG.backendUrl, |
||||
headers: { |
||||
'Content-Type': 'application/json' |
||||
}, |
||||
data: JSON.stringify(data), |
||||
onload: function(response) { |
||||
if (response.status >= 200 && response.status < 300) { |
||||
resolve(response); |
||||
} else { |
||||
reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); |
||||
} |
||||
}, |
||||
onerror: function(error) { |
||||
reject(error); |
||||
}, |
||||
timeout: 10000 |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
const sendAllData = async () => { |
||||
updateStatus('整理数据并发送到后端...'); |
||||
|
||||
const finalImages = {}; |
||||
let totalCount = 0; |
||||
|
||||
const sortedPages = Object.keys(allImages).map(Number).sort((a, b) => a - b); |
||||
|
||||
for (const page of sortedPages) { |
||||
if (allImages[page]) { |
||||
for (const imgUrl of allImages[page]) { |
||||
const key = String(imgIndex).padStart(4, '0'); |
||||
finalImages[key] = imgUrl; |
||||
imgIndex++; |
||||
totalCount++; |
||||
} |
||||
} |
||||
} |
||||
|
||||
const data = { |
||||
title: document.title || '无标题', |
||||
source: source, |
||||
url: buildPageUrl(1), |
||||
totalPages: sortedPages.length, |
||||
totalImages: totalCount, |
||||
imgs: finalImages |
||||
}; |
||||
|
||||
console.log('准备发送的数据:', data); |
||||
|
||||
try { |
||||
await sendToBackend(data); |
||||
updateStatus(`✅ 发送成功!<br>共 ${sortedPages.length} 页<br>${totalCount} 张图片`); |
||||
return true; |
||||
} catch (error) { |
||||
updateStatus(`❌ 发送失败: ${error.message}`); |
||||
return false; |
||||
} |
||||
}; |
||||
|
||||
const beginPageProcessing = async () => { |
||||
const isCrawlSession = sessionStorage.getItem('hd4k_crawling') === 'true'; |
||||
|
||||
if (!isCrawlSession) { |
||||
console.log('不在爬取会话中,停止处理'); |
||||
return; |
||||
} |
||||
|
||||
const currentUrl = window.location.href; |
||||
|
||||
if (crawledUrls.includes(currentUrl)) { |
||||
updateStatus('检测到重复URL,停止爬取'); |
||||
await finishCrawling(); |
||||
return; |
||||
} |
||||
|
||||
crawledUrls.push(currentUrl); |
||||
sessionStorage.setItem('hd4k_crawled_urls', JSON.stringify(crawledUrls)); |
||||
|
||||
const urlPageNum = getCurrentPageNumber(); |
||||
|
||||
if (currentPage !== urlPageNum) { |
||||
currentPage = urlPageNum; |
||||
} |
||||
|
||||
updateStatus(`处理第 ${currentPage} 页...`); |
||||
const imageUrls = getCurrentPageImages(); |
||||
console.log(`第 ${currentPage} 页找到 ${imageUrls.length} 张图片`); |
||||
|
||||
if (imageUrls.length > 0) { |
||||
allImages[currentPage] = imageUrls; |
||||
sessionStorage.setItem('hd4k_all_images', JSON.stringify(allImages)); |
||||
updateStatus(`第 ${currentPage} 页: 找到 ${imageUrls.length} 张图片`); |
||||
|
||||
setTimeout(async () => { |
||||
const nextPage = currentPage + 1; |
||||
|
||||
if (nextPage > CONFIG.maxPages) { |
||||
updateStatus(`已达到最大页数 ${CONFIG.maxPages}`); |
||||
await finishCrawling(); |
||||
return; |
||||
} |
||||
|
||||
const nextUrl = buildPageUrl(nextPage); |
||||
|
||||
if (crawledUrls.includes(nextUrl)) { |
||||
updateStatus('下一页URL已爬取过,停止爬取'); |
||||
await finishCrawling(); |
||||
return; |
||||
} |
||||
|
||||
updateStatus(`准备跳转到第 ${nextPage} 页`); |
||||
sessionStorage.setItem('hd4k_current_page', nextPage.toString()); |
||||
|
||||
setTimeout(() => { |
||||
window.location.href = nextUrl; |
||||
}, CONFIG.pageDelay); |
||||
|
||||
}, CONFIG.pageDelay); |
||||
|
||||
} else { |
||||
updateStatus(`第 ${currentPage} 页: 无图片`); |
||||
|
||||
setTimeout(async () => { |
||||
await finishCrawling(); |
||||
}, CONFIG.pageDelay); |
||||
} |
||||
}; |
||||
|
||||
const startCrawling = async () => { |
||||
if (isCrawling) { |
||||
alert('正在爬取中,请稍候...'); |
||||
return; |
||||
} |
||||
|
||||
const button = document.getElementById('hd4k-btn'); |
||||
button.textContent = '爬取中...'; |
||||
button.style.backgroundColor = '#ff9800'; |
||||
button.disabled = true; |
||||
|
||||
isCrawling = true; |
||||
allImages = {}; |
||||
crawledUrls = []; |
||||
currentPage = 1; |
||||
imgIndex = 1; |
||||
|
||||
sessionStorage.removeItem('hd4k_all_images'); |
||||
sessionStorage.removeItem('hd4k_crawled_urls'); |
||||
|
||||
updateStatus('开始自动翻页爬取...'); |
||||
|
||||
const firstPageUrl = buildPageUrl(1); |
||||
const currentUrl = window.location.href; |
||||
|
||||
sessionStorage.setItem('hd4k_crawling', 'true'); |
||||
sessionStorage.setItem('hd4k_current_page', '1'); |
||||
|
||||
if (currentUrl !== firstPageUrl) { |
||||
updateStatus(`跳转到第一页`); |
||||
window.location.href = firstPageUrl; |
||||
return; |
||||
} |
||||
|
||||
beginPageProcessing(); |
||||
}; |
||||
|
||||
const finishCrawling = async () => { |
||||
sessionStorage.removeItem('hd4k_crawling'); |
||||
sessionStorage.removeItem('hd4k_current_page'); |
||||
|
||||
if (Object.keys(allImages).length > 0) { |
||||
await sendAllData(); |
||||
} else { |
||||
updateStatus('未找到任何图片数据'); |
||||
} |
||||
|
||||
const button = document.getElementById('hd4k-btn'); |
||||
button.textContent = '开始爬取'; |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.disabled = false; |
||||
isCrawling = false; |
||||
|
||||
setTimeout(() => { |
||||
const statusDiv = document.getElementById('hd4k-status'); |
||||
if (statusDiv) { |
||||
statusDiv.style.display = 'none'; |
||||
} |
||||
}, 5000); |
||||
}; |
||||
|
||||
const onPageLoad = () => { |
||||
const isCrawlSession = sessionStorage.getItem('hd4k_crawling') === 'true'; |
||||
|
||||
if (isCrawlSession) { |
||||
isCrawling = true; |
||||
currentPage = getCurrentPageNumber(); |
||||
|
||||
const savedImages = sessionStorage.getItem('hd4k_all_images'); |
||||
const savedUrls = sessionStorage.getItem('hd4k_crawled_urls'); |
||||
|
||||
if (savedImages) { |
||||
allImages = JSON.parse(savedImages); |
||||
} |
||||
|
||||
if (savedUrls) { |
||||
crawledUrls = JSON.parse(savedUrls); |
||||
} |
||||
|
||||
setTimeout(() => { |
||||
beginPageProcessing(); |
||||
}, 1500); |
||||
} |
||||
}; |
||||
|
||||
const init = () => { |
||||
if (!document.getElementById('hd4k-btn')) { |
||||
const button = createButton(); |
||||
const statusDiv = createStatusDisplay(); |
||||
|
||||
document.body.appendChild(button); |
||||
document.body.appendChild(statusDiv); |
||||
|
||||
updateStatus('HD4K下载器已加载<br>点击按钮开始自动翻页爬取'); |
||||
setTimeout(() => { |
||||
const statusDiv = document.getElementById('hd4k-status'); |
||||
if (statusDiv) { |
||||
statusDiv.style.display = 'none'; |
||||
} |
||||
}, 3000); |
||||
|
||||
onPageLoad(); |
||||
} |
||||
}; |
||||
|
||||
if (document.readyState === 'loading') { |
||||
document.addEventListener('DOMContentLoaded', init); |
||||
} else { |
||||
const isCrawlSession = sessionStorage.getItem('hd4k_crawling') === 'true'; |
||||
if (isCrawlSession) { |
||||
isCrawling = true; |
||||
const button = createButton(); |
||||
button.textContent = '爬取中...'; |
||||
button.style.backgroundColor = '#ff9800'; |
||||
button.disabled = true; |
||||
document.body.appendChild(button); |
||||
|
||||
const statusDiv = createStatusDisplay(); |
||||
document.body.appendChild(statusDiv); |
||||
updateStatus('检测到未完成的爬取任务,继续执行...'); |
||||
|
||||
setTimeout(onPageLoad, 1500); |
||||
} else { |
||||
init(); |
||||
} |
||||
} |
||||
|
||||
})(); |
||||
@ -1,158 +0,0 @@ |
||||
// ==UserScript==
|
||||
// @name hd4k_downloader
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 1.0
|
||||
// @description 提取页面图片并发送到后端
|
||||
// @author Jack
|
||||
// @match *://*/*
|
||||
// @grant GM_xmlhttpRequest
|
||||
// ==/UserScript==
|
||||
|
||||
(function() { |
||||
'use strict'; |
||||
|
||||
// 创建按钮
|
||||
const createButton = () => { |
||||
const button = document.createElement('button'); |
||||
button.textContent = '提取图片'; |
||||
button.id = 'hd4k-extract-btn'; |
||||
|
||||
// 按钮样式 - 修改为蓝色系
|
||||
button.style.position = 'fixed'; |
||||
button.style.top = '12.5%'; |
||||
button.style.right = '1%'; |
||||
button.style.transform = 'translateY(-50%)'; |
||||
button.style.padding = '6px 12px'; |
||||
button.style.fontSize = '12px'; |
||||
button.style.fontWeight = 'bold'; |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.style.color = '#fff'; |
||||
button.style.border = 'none'; |
||||
button.style.borderRadius = '8px'; |
||||
button.style.cursor = 'pointer'; |
||||
button.style.zIndex = '10000'; |
||||
button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; |
||||
button.style.transition = 'all 0.3s ease'; |
||||
|
||||
// 悬停效果
|
||||
button.addEventListener('mouseenter', () => { |
||||
button.style.backgroundColor = '#1a6ee0'; |
||||
button.style.transform = 'translateY(-50%) scale(1.05)'; |
||||
}); |
||||
|
||||
button.addEventListener('mouseleave', () => { |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.style.transform = 'translateY(-50%) scale(1)'; |
||||
}); |
||||
|
||||
// 点击事件
|
||||
button.addEventListener('click', extractAndSendImages); |
||||
|
||||
return button; |
||||
}; |
||||
|
||||
// 获取所有图片URL
|
||||
const getAllImageUrls = () => { |
||||
const images = document.querySelectorAll('img'); |
||||
const imageUrls = {}; |
||||
let index = 1; |
||||
|
||||
images.forEach(img => { |
||||
let src = img.src || img.dataset.src || img.currentSrc; |
||||
|
||||
// 过滤掉空URL、base64和数据URL
|
||||
if (src && !src.startsWith('data:') && !src.startsWith('blob:')) { |
||||
const key = String(index).padStart(4, '0'); |
||||
imageUrls[key] = src; |
||||
index++; |
||||
} |
||||
}); |
||||
|
||||
return imageUrls; |
||||
}; |
||||
|
||||
// 提取并发送数据
|
||||
const extractAndSendImages = () => { |
||||
try { |
||||
// 获取当前页面信息
|
||||
const title = document.title || '无标题'; |
||||
const url = window.location.href; |
||||
const imageUrls = getAllImageUrls(); |
||||
|
||||
// 准备数据
|
||||
const data = { |
||||
title: title, |
||||
url: url, |
||||
imgs: imageUrls |
||||
}; |
||||
|
||||
console.log('提取的数据:', data); |
||||
|
||||
// 显示加载状态
|
||||
const button = document.getElementById('hd4k-extract-btn'); |
||||
const originalText = button.textContent; |
||||
button.textContent = '处理中...'; |
||||
button.disabled = true; |
||||
|
||||
// 发送到后端
|
||||
GM_xmlhttpRequest({ |
||||
method: 'POST', |
||||
url: 'http://127.0.0.1:55830/api/save_imgs', |
||||
headers: { |
||||
'Content-Type': 'application/json' |
||||
}, |
||||
data: JSON.stringify(data), |
||||
onload: function(response) { |
||||
console.log('发送成功:', response); |
||||
|
||||
// 恢复按钮状态并显示成功
|
||||
button.textContent = '成功!'; |
||||
button.style.backgroundColor = '#28a745'; |
||||
|
||||
setTimeout(() => { |
||||
button.textContent = originalText; |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.disabled = false; |
||||
}, 1500); |
||||
}, |
||||
onerror: function(error) { |
||||
console.error('发送失败:', error); |
||||
|
||||
// 恢复按钮状态并显示错误
|
||||
button.textContent = '失败!'; |
||||
button.style.backgroundColor = '#dc3545'; |
||||
|
||||
setTimeout(() => { |
||||
button.textContent = originalText; |
||||
button.style.backgroundColor = '#2c80ff'; |
||||
button.disabled = false; |
||||
}, 1500); |
||||
|
||||
alert('发送失败,请检查后端服务是否运行: ' + error.statusText); |
||||
}, |
||||
timeout: 10000 |
||||
}); |
||||
|
||||
} catch (error) { |
||||
console.error('提取图片时出错:', error); |
||||
alert('提取图片时出错: ' + error.message); |
||||
} |
||||
}; |
||||
|
||||
// 初始化 - 添加按钮到页面
|
||||
const init = () => { |
||||
// 确保按钮不会重复添加
|
||||
if (!document.getElementById('hd4k-extract-btn')) { |
||||
const button = createButton(); |
||||
document.body.appendChild(button); |
||||
} |
||||
}; |
||||
|
||||
// 页面加载完成后初始化
|
||||
if (document.readyState === 'loading') { |
||||
document.addEventListener('DOMContentLoaded', init); |
||||
} else { |
||||
init(); |
||||
} |
||||
|
||||
})(); |
||||
@ -1,84 +0,0 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="zh-CN"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>HD4K 下载管理</title> |
||||
<link rel="stylesheet" href="index.css"> |
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<header> |
||||
<h1><i class="fas fa-download"></i> HD4K 下载管理</h1> |
||||
<p class="subtitle">管理未完成的下载任务</p> |
||||
</header> |
||||
|
||||
<div class="control-panel"> |
||||
<button id="reloadBtn" class="btn btn-primary"> |
||||
<i class="fas fa-sync-alt"></i> 重载下载文件夹 |
||||
</button> |
||||
<button id="cleanupBtn" class="btn btn-warning"> |
||||
<i class="fas fa-trash-alt"></i> 清理JSON文件 |
||||
</button> |
||||
<div class="status" id="statusMessage"></div> |
||||
</div> |
||||
|
||||
<div class="folders-container"> |
||||
<h2><i class="fas fa-folder-open"></i> 未完成的任务</h2> |
||||
<div id="foldersList" class="folders-list"> |
||||
<!-- 文件夹列表将在这里动态生成 --> |
||||
</div> |
||||
<div id="noFolders" class="no-folders"> |
||||
<i class="fas fa-check-circle"></i> |
||||
<p>所有下载任务已完成!</p> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="api-info"> |
||||
<h3><i class="fas fa-info-circle"></i> API 信息</h3> |
||||
<p>图片下载API: <code>POST http://127.0.0.1:<span id="port">55830</span>/api/save_imgs</code></p> |
||||
<p>请求格式: <code>{"title": "漫画标题", "imgs": {"001": "url1", "002": "url2"}}</code></p> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 下载进度模态框 --> |
||||
<div id="downloadModal" class="modal"> |
||||
<div class="modal-content"> |
||||
<div class="modal-header"> |
||||
<h3 id="modalTitle">下载进度</h3> |
||||
<button class="close-btn">×</button> |
||||
</div> |
||||
<div class="modal-body"> |
||||
<div class="progress-container"> |
||||
<div class="progress-bar" id="progressBar"></div> |
||||
<div class="progress-text" id="progressText">0%</div> |
||||
</div> |
||||
<div class="download-stats"> |
||||
<div class="stat-item"> |
||||
<span class="stat-label">总文件数:</span> |
||||
<span class="stat-value" id="totalFiles">0</span> |
||||
</div> |
||||
<div class="stat-item"> |
||||
<span class="stat-label">已下载:</span> |
||||
<span class="stat-value success" id="downloadedFiles">0</span> |
||||
</div> |
||||
<div class="stat-item"> |
||||
<span class="stat-label">待下载:</span> |
||||
<span class="stat-value pending" id="pendingFiles">0</span> |
||||
</div> |
||||
</div> |
||||
<div class="log-container"> |
||||
<h4>下载日志</h4> |
||||
<div class="log-content" id="downloadLog"></div> |
||||
</div> |
||||
</div> |
||||
<div class="modal-footer"> |
||||
<button id="closeModalBtn" class="btn btn-secondary">关闭</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script src="index.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -1,255 +0,0 @@ |
||||
class DownloadManager { |
||||
constructor() { |
||||
this.port = window.location.port || '55830'; |
||||
this.baseUrl = `http://127.0.0.1:${this.port}/api`; |
||||
this.currentDownload = null; |
||||
|
||||
this.init(); |
||||
} |
||||
|
||||
init() { |
||||
document.getElementById('port').textContent = this.port; |
||||
|
||||
// 绑定事件
|
||||
document.getElementById('reloadBtn').addEventListener('click', () => this.reloadFolders()); |
||||
document.getElementById('cleanupBtn').addEventListener('click', () => this.cleanupJson()); |
||||
document.getElementById('closeModalBtn').addEventListener('click', () => this.hideModal()); |
||||
document.querySelector('.close-btn').addEventListener('click', () => this.hideModal()); |
||||
|
||||
// 点击模态框外部关闭
|
||||
document.getElementById('downloadModal').addEventListener('click', (e) => { |
||||
if (e.target === document.getElementById('downloadModal')) { |
||||
this.hideModal(); |
||||
} |
||||
}); |
||||
|
||||
// 初始加载
|
||||
this.reloadFolders(); |
||||
} |
||||
|
||||
showStatus(message, type = 'info') { |
||||
const statusEl = document.getElementById('statusMessage'); |
||||
statusEl.textContent = message; |
||||
statusEl.style.borderLeftColor = type === 'error' ? '#ef4444' : |
||||
type === 'warning' ? '#f59e0b' : '#10b981'; |
||||
} |
||||
|
||||
showModal(title) { |
||||
document.getElementById('modalTitle').textContent = title; |
||||
document.getElementById('downloadModal').style.display = 'flex'; |
||||
this.resetModal(); |
||||
} |
||||
|
||||
hideModal() { |
||||
document.getElementById('downloadModal').style.display = 'none'; |
||||
if (this.currentDownload && this.currentDownload.abort) { |
||||
this.currentDownload.abort(); |
||||
} |
||||
} |
||||
|
||||
resetModal() { |
||||
document.getElementById('progressBar').style.width = '0%'; |
||||
document.getElementById('progressText').textContent = '0%'; |
||||
document.getElementById('totalFiles').textContent = '0'; |
||||
document.getElementById('downloadedFiles').textContent = '0'; |
||||
document.getElementById('pendingFiles').textContent = '0'; |
||||
document.getElementById('downloadLog').innerHTML = ''; |
||||
} |
||||
|
||||
updateProgress(progress, stats) { |
||||
document.getElementById('progressBar').style.width = `${progress}%`; |
||||
document.getElementById('progressText').textContent = `${Math.round(progress)}%`; |
||||
|
||||
if (stats) { |
||||
document.getElementById('totalFiles').textContent = stats.total || 0; |
||||
document.getElementById('downloadedFiles').textContent = stats.downloaded || 0; |
||||
document.getElementById('pendingFiles').textContent = stats.pending || 0; |
||||
} |
||||
} |
||||
|
||||
addLogEntry(message, type = 'info') { |
||||
const logEl = document.getElementById('downloadLog'); |
||||
const entry = document.createElement('div'); |
||||
entry.className = `log-entry ${type}`; |
||||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; |
||||
logEl.appendChild(entry); |
||||
logEl.scrollTop = logEl.scrollHeight; |
||||
} |
||||
|
||||
async reloadFolders() { |
||||
try { |
||||
this.showStatus('正在加载文件夹...', 'info'); |
||||
document.getElementById('reloadBtn').disabled = true; |
||||
|
||||
const response = await fetch(`${this.baseUrl}/reload_folders`); |
||||
const data = await response.json(); |
||||
|
||||
if (data.success) { |
||||
this.displayFolders(data.folders); |
||||
this.showStatus(`已加载 ${data.folders.length} 个未完成的任务`, 'success'); |
||||
} else { |
||||
throw new Error(data.message || '加载失败'); |
||||
} |
||||
} catch (error) { |
||||
console.error('加载失败:', error); |
||||
this.showStatus(`加载失败: ${error.message}`, 'error'); |
||||
this.displayFolders([]); |
||||
} finally { |
||||
document.getElementById('reloadBtn').disabled = false; |
||||
} |
||||
} |
||||
|
||||
displayFolders(folders) { |
||||
const foldersList = document.getElementById('foldersList'); |
||||
const noFolders = document.getElementById('noFolders'); |
||||
|
||||
// 过滤掉进度为100%的文件夹
|
||||
const incompleteFolders = folders.filter(folder => folder.progress < 100); |
||||
|
||||
if (incompleteFolders.length === 0) { |
||||
foldersList.innerHTML = ''; |
||||
noFolders.style.display = 'block'; |
||||
return; |
||||
} |
||||
|
||||
noFolders.style.display = 'none'; |
||||
|
||||
foldersList.innerHTML = incompleteFolders.map(folder => ` |
||||
<div class="folder-item"> |
||||
<div class="folder-info"> |
||||
<div class="folder-title"> |
||||
<i class="fas fa-book"></i> |
||||
${this.escapeHtml(folder.title)} |
||||
</div> |
||||
<div class="folder-stats"> |
||||
<span>总文件: ${folder.total}</span> |
||||
<span>已下载: ${folder.downloaded}</span> |
||||
<span>缺失: ${folder.total - folder.downloaded}</span> |
||||
</div> |
||||
<div class="progress-container"> |
||||
<div class="progress-wrapper"> |
||||
<div class="progress"> |
||||
<div class="progress-fill" style="width: ${folder.progress}%"></div> |
||||
</div> |
||||
<div class="progress-text">${folder.progress.toFixed(1)}%</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="folder-actions"> |
||||
<button class="btn btn-primary btn-small" onclick="downloadManager.downloadFolder('${this.escapeAttr(folder.folder)}', '${this.escapeAttr(folder.title)}')"> |
||||
<i class="fas fa-download"></i> 下载缺失文件 (${folder.total - folder.downloaded}) |
||||
</button> |
||||
</div> |
||||
</div> |
||||
`).join('');
|
||||
} |
||||
|
||||
async downloadFolder(folderPath, title) { |
||||
try { |
||||
this.showModal(`正在下载: ${title}`); |
||||
this.addLogEntry('开始下载缺失文件...', 'info'); |
||||
|
||||
console.log('发送请求:', { folder: folderPath, title: title }); |
||||
|
||||
const response = await fetch(`${this.baseUrl}/download_missing`, { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify({ |
||||
folder: folderPath, |
||||
title: title |
||||
}) |
||||
}); |
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) { |
||||
const errorText = await response.text(); |
||||
console.error('HTTP错误:', response.status, errorText); |
||||
throw new Error(`HTTP ${response.status}: ${errorText}`); |
||||
} |
||||
|
||||
const data = await response.json(); |
||||
console.log('收到响应:', data); |
||||
|
||||
if (data.success) { |
||||
// 更新进度
|
||||
const totalSaved = (data.saved || 0) + (data.skipped || 0); |
||||
const total = data.total || 1; |
||||
const progress = totalSaved / total * 100; |
||||
|
||||
this.updateProgress(progress, { |
||||
total: total, |
||||
downloaded: totalSaved, |
||||
pending: data.failed || 0 |
||||
}); |
||||
|
||||
// 添加日志
|
||||
if (data.details) { |
||||
data.details.forEach(detail => { |
||||
if (detail.status === 'success') { |
||||
this.addLogEntry(`成功下载: ${detail.key}`, 'success'); |
||||
} else if (detail.status === 'failed') { |
||||
this.addLogEntry(`下载失败 ${detail.key}: ${detail.message}`, 'error'); |
||||
} else if (detail.status === 'skipped') { |
||||
this.addLogEntry(`跳过: ${detail.key} (${detail.message})`, 'info'); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
this.addLogEntry(`下载完成: ${data.message}`, 'success'); |
||||
|
||||
// 刷新文件夹列表
|
||||
setTimeout(() => this.reloadFolders(), 1000); |
||||
} else { |
||||
this.addLogEntry(`下载失败: ${data.message}`, 'error'); |
||||
} |
||||
} catch (error) { |
||||
console.error('下载失败:', error); |
||||
this.addLogEntry(`下载失败: ${error.message}`, 'error'); |
||||
} |
||||
} |
||||
|
||||
async cleanupJson() { |
||||
if (!confirm('确定要删除所有JSON文件吗?此操作不可恢复。')) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
this.showStatus('正在清理JSON文件...', 'info'); |
||||
document.getElementById('cleanupBtn').disabled = true; |
||||
|
||||
const response = await fetch(`${this.baseUrl}/cleanup_json`, { |
||||
method: 'POST' |
||||
}); |
||||
|
||||
const data = await response.json(); |
||||
|
||||
if (data.success) { |
||||
this.showStatus(data.message, 'success'); |
||||
// 刷新文件夹列表
|
||||
setTimeout(() => this.reloadFolders(), 500); |
||||
} else { |
||||
throw new Error(data.message || '清理失败'); |
||||
} |
||||
} catch (error) { |
||||
console.error('清理失败:', error); |
||||
this.showStatus(`清理失败: ${error.message}`, 'error'); |
||||
} finally { |
||||
document.getElementById('cleanupBtn').disabled = false; |
||||
} |
||||
} |
||||
|
||||
escapeHtml(text) { |
||||
const div = document.createElement('div'); |
||||
div.textContent = text; |
||||
return div.innerHTML; |
||||
} |
||||
|
||||
escapeAttr(text) { |
||||
return this.escapeHtml(text).replace(/"/g, '"'); |
||||
} |
||||
} |
||||
|
||||
// 初始化应用
|
||||
const downloadManager = new DownloadManager(); |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,44 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="zh-CN"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>HD4K 下载管理</title> |
||||
<link rel="stylesheet" href="index.css"> |
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<header> |
||||
<h1><i class="fas fa-download"></i> HD4K 下载管理</h1> |
||||
<p class="subtitle">管理未完成的下载任务</p> |
||||
</header> |
||||
|
||||
<div class="control-panel"> |
||||
<button id="reloadBtn" class="btn btn-primary"> |
||||
<i class="fas fa-sync-alt"></i> 读取文件 |
||||
</button> |
||||
<button id="downloadAllBtn" class="btn btn-success"> |
||||
<i class="fas fa-cloud-download-alt"></i> 下载全部 |
||||
</button> |
||||
<button id="cleanupBtn" class="btn btn-warning"> |
||||
<i class="fas fa-trash-alt"></i> 清理JSON文件 |
||||
</button> |
||||
<div class="status" id="statusMessage"></div> |
||||
</div> |
||||
|
||||
<div class="folders-container"> |
||||
<h2><i class="fas fa-folder-open"></i> 未完成的任务</h2> |
||||
<div id="foldersList" class="folders-list"> |
||||
<!-- 文件夹列表将在这里动态生成 --> |
||||
</div> |
||||
<div id="noFolders" class="no-folders"> |
||||
<i class="fas fa-check-circle"></i> |
||||
<p>所有下载任务已完成!</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script src="index.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,274 @@ |
||||
class DownloadManager { |
||||
constructor() { |
||||
// 使用默认端口
|
||||
this.port = window.location.port || '55830'; |
||||
this.baseUrl = `http://127.0.0.1:${this.port}/api`; |
||||
this.currentFolders = []; |
||||
|
||||
console.log('DownloadManager初始化完成,baseUrl:', this.baseUrl); |
||||
|
||||
this.init(); |
||||
} |
||||
|
||||
init() { |
||||
// 绑定事件
|
||||
const reloadBtn = document.getElementById('reloadBtn'); |
||||
const downloadAllBtn = document.getElementById('downloadAllBtn'); |
||||
const cleanupBtn = document.getElementById('cleanupBtn'); |
||||
|
||||
if (reloadBtn) { |
||||
reloadBtn.addEventListener('click', () => { |
||||
console.log('点击了读取文件按钮'); |
||||
this.reloadFolders(); |
||||
}); |
||||
} else { |
||||
console.error('未找到reloadBtn按钮'); |
||||
} |
||||
|
||||
if (downloadAllBtn) { |
||||
downloadAllBtn.addEventListener('click', () => { |
||||
console.log('点击了下载全部按钮'); |
||||
this.downloadAll(); |
||||
}); |
||||
} else { |
||||
console.error('未找到downloadAllBtn按钮'); |
||||
} |
||||
|
||||
if (cleanupBtn) { |
||||
cleanupBtn.addEventListener('click', () => { |
||||
console.log('点击了清理JSON文件按钮'); |
||||
this.cleanupJson(); |
||||
}); |
||||
} else { |
||||
console.error('未找到cleanupBtn按钮'); |
||||
} |
||||
|
||||
console.log('事件绑定完成'); |
||||
|
||||
// 初始加载
|
||||
setTimeout(() => { |
||||
this.reloadFolders(); |
||||
}, 500); |
||||
} |
||||
|
||||
showStatus(message, type = 'info') { |
||||
const statusEl = document.getElementById('statusMessage'); |
||||
if (statusEl) { |
||||
console.log('状态消息:', message, '类型:', type); |
||||
statusEl.textContent = message; |
||||
statusEl.style.borderLeftColor = type === 'error' ? '#ef4444' : |
||||
type === 'warning' ? '#f59e0b' : '#10b981'; |
||||
} else { |
||||
console.log('状态消息(找不到显示元素):', message); |
||||
} |
||||
} |
||||
|
||||
async reloadFolders() { |
||||
try { |
||||
this.showStatus('正在读取文件...', 'info'); |
||||
|
||||
const reloadBtn = document.getElementById('reloadBtn'); |
||||
if (reloadBtn) { |
||||
reloadBtn.disabled = true; |
||||
} |
||||
|
||||
const url = `${this.baseUrl}/reload_folders`; |
||||
console.log('发送请求到:', url); |
||||
|
||||
const response = await fetch(url); |
||||
console.log('响应状态:', response.status, response.statusText); |
||||
|
||||
if (!response.ok) { |
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
||||
} |
||||
|
||||
const data = await response.json(); |
||||
console.log('收到响应数据:', data); |
||||
|
||||
if (data.success) { |
||||
this.currentFolders = data.folders || []; |
||||
this.displayFolders(this.currentFolders); |
||||
const pendingCount = this.currentFolders.reduce((sum, folder) => sum + (folder.pending || 0), 0); |
||||
this.showStatus(`已读取 ${this.currentFolders.length} 个任务,共 ${pendingCount} 个文件待下载`, 'success'); |
||||
} else { |
||||
throw new Error(data.message || '加载失败'); |
||||
} |
||||
} catch (error) { |
||||
console.error('加载失败:', error); |
||||
this.showStatus(`加载失败: ${error.message}`, 'error'); |
||||
this.displayFolders([]); |
||||
} finally { |
||||
const reloadBtn = document.getElementById('reloadBtn'); |
||||
if (reloadBtn) { |
||||
reloadBtn.disabled = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
displayFolders(folders) { |
||||
const foldersList = document.getElementById('foldersList'); |
||||
const noFolders = document.getElementById('noFolders'); |
||||
|
||||
if (!foldersList || !noFolders) { |
||||
console.error('找不到文件夹列表容器'); |
||||
return; |
||||
} |
||||
|
||||
const incompleteFolders = folders.filter(folder => (folder.progress || 0) < 100); |
||||
|
||||
if (incompleteFolders.length === 0) { |
||||
foldersList.innerHTML = ''; |
||||
noFolders.style.display = 'block'; |
||||
return; |
||||
} |
||||
|
||||
noFolders.style.display = 'none'; |
||||
|
||||
foldersList.innerHTML = incompleteFolders.map(folder => ` |
||||
<div class="folder-item"> |
||||
<div class="folder-info"> |
||||
<div class="folder-title"> |
||||
<i class="fas fa-book"></i> |
||||
${this.escapeHtml(folder.title || '未命名')} |
||||
<span style="font-size: 0.9rem; color: #64748b; margin-left: 10px;"> |
||||
(${folder.web_site || 'default'}) |
||||
</span> |
||||
</div> |
||||
<div class="folder-stats"> |
||||
<span>总文件: ${folder.total || 0}</span> |
||||
<span>已下载: ${folder.downloaded || 0}</span> |
||||
<span style="color: #f59e0b; font-weight: 600;">待下载: ${folder.pending || 0}</span> |
||||
</div> |
||||
<div class="progress-container"> |
||||
<div class="progress-wrapper"> |
||||
<div class="progress"> |
||||
<div class="progress-fill" style="width: ${folder.progress || 0}%"></div> |
||||
</div> |
||||
<div class="progress-text">${(folder.progress || 0).toFixed(1)}%</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
`).join('');
|
||||
} |
||||
|
||||
async downloadAll() { |
||||
if (this.currentFolders.length === 0) { |
||||
this.showStatus('没有需要下载的任务', 'warning'); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
this.showStatus('开始下载所有任务...', 'info'); |
||||
const downloadAllBtn = document.getElementById('downloadAllBtn'); |
||||
if (downloadAllBtn) { |
||||
downloadAllBtn.disabled = true; |
||||
} |
||||
|
||||
// 依次下载每个文件夹
|
||||
for (let i = 0; i < this.currentFolders.length; i++) { |
||||
const folder = this.currentFolders[i]; |
||||
if ((folder.pending || 0) > 0) { |
||||
await this.downloadFolder(folder); |
||||
} |
||||
} |
||||
|
||||
this.showStatus('所有任务下载完成', 'success'); |
||||
|
||||
// 等待1秒后自动刷新
|
||||
setTimeout(() => { |
||||
this.reloadFolders(); |
||||
}, 1000); |
||||
|
||||
} catch (error) { |
||||
console.error('下载失败:', error); |
||||
this.showStatus(`下载失败: ${error.message}`, 'error'); |
||||
} finally { |
||||
const downloadAllBtn = document.getElementById('downloadAllBtn'); |
||||
if (downloadAllBtn) { |
||||
downloadAllBtn.disabled = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
async downloadFolder(folder) { |
||||
try { |
||||
const pathParts = folder.folder.split('/'); |
||||
let webSiteName = 'default'; |
||||
if (pathParts.length >= 2) { |
||||
webSiteName = pathParts[pathParts.length - 2]; |
||||
} |
||||
|
||||
const response = await fetch(`${this.baseUrl}/download_missing`, { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify({ |
||||
folder: folder.folder, |
||||
title: folder.title || '未命名', |
||||
web_site: webSiteName |
||||
}) |
||||
}); |
||||
|
||||
if (!response.ok) { |
||||
const errorText = await response.text(); |
||||
throw new Error(`HTTP ${response.status}: ${errorText}`); |
||||
} |
||||
|
||||
const data = await response.json(); |
||||
|
||||
if (!data.success) { |
||||
console.warn(`文件夹 ${folder.title} 下载失败: ${data.message}`); |
||||
} |
||||
|
||||
} catch (error) { |
||||
console.error(`下载文件夹 ${folder.title} 失败:`, error); |
||||
} |
||||
} |
||||
|
||||
async cleanupJson() { |
||||
if (!confirm('确定要删除所有JSON文件吗?此操作不可恢复。')) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
this.showStatus('正在清理JSON文件...', 'info'); |
||||
const cleanupBtn = document.getElementById('cleanupBtn'); |
||||
if (cleanupBtn) { |
||||
cleanupBtn.disabled = true; |
||||
} |
||||
|
||||
const response = await fetch(`${this.baseUrl}/cleanup_json`, { |
||||
method: 'POST' |
||||
}); |
||||
|
||||
const data = await response.json(); |
||||
|
||||
if (data.success) { |
||||
this.showStatus(data.message, 'success'); |
||||
// 刷新文件夹列表
|
||||
setTimeout(() => this.reloadFolders(), 500); |
||||
} else { |
||||
throw new Error(data.message || '清理失败'); |
||||
} |
||||
} catch (error) { |
||||
console.error('清理失败:', error); |
||||
this.showStatus(`清理失败: ${error.message}`, 'error'); |
||||
} finally { |
||||
const cleanupBtn = document.getElementById('cleanupBtn'); |
||||
if (cleanupBtn) { |
||||
cleanupBtn.disabled = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
escapeHtml(text) { |
||||
const div = document.createElement('div'); |
||||
div.textContent = text; |
||||
return div.innerHTML; |
||||
} |
||||
} |
||||
|
||||
// 初始化应用
|
||||
const downloadManager = new DownloadManager(); |
||||
Loading…
Reference in new issue