jack 3 months ago
parent c166be9e62
commit 92010207b5
  1. 16
      Readme.md
  2. 420
      Tampermonkey/hdk4.js
  3. 158
      hdk4_downloader.js
  4. 84
      index.html
  5. 255
      index.js
  6. 337
      main.go
  7. 1702
      prompt.txt
  8. 180
      src/index.css
  9. 44
      src/index.html
  10. 274
      src/index.js

@ -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">&times;</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, '&quot;');
}
}
// 初始化应用
const downloadManager = new DownloadManager();

@ -22,6 +22,7 @@ var (
type SaveImagesRequest struct {
Title string `json:"title"`
WebSiteName string `json:"source"`
Imgs map[string]string `json:"imgs"`
}
@ -39,6 +40,7 @@ type SaveImagesResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Title string `json:"title"`
WebSite string `json:"web_site"`
Folder string `json:"folder"`
JsonPath string `json:"json_path"`
Total int `json:"total"`
@ -51,20 +53,42 @@ type SaveImagesResponse struct {
// 文件夹信息结构
type FolderInfo struct {
Title string `json:"title"`
WebSite string `json:"web_site"`
Folder string `json:"folder"`
JsonPath string `json:"json_path"`
Total int `json:"total"`
Downloaded int `json:"downloaded"`
Progress float64 `json:"progress"`
Pending int `json:"pending"`
}
// 加载JSON数据
type JsonData struct {
Title string `json:"title"`
WebSiteName string `json:"source"`
Imgs map[string]string `json:"imgs"`
}
func saveImagesHandler(w http.ResponseWriter, r *http.Request) {
type SaveJsonResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Title string `json:"title"`
WebSite string `json:"web_site"`
Folder string `json:"folder"`
JsonPath string `json:"json_path"`
Total int `json:"total"`
}
func mainHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, "./src/index.html")
return
}
http.FileServer(http.Dir("./src")).ServeHTTP(w, r)
}
func saveJsonHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
@ -75,7 +99,6 @@ func saveImagesHandler(w http.ResponseWriter, r *http.Request) {
return
}
// 解析请求
var req SaveImagesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
@ -87,7 +110,6 @@ func saveImagesHandler(w http.ResponseWriter, r *http.Request) {
return
}
// 验证参数
if req.Title == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
@ -97,18 +119,8 @@ func saveImagesHandler(w http.ResponseWriter, r *http.Request) {
})
return
}
if len(req.Imgs) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "图片列表不能为空",
})
return
}
// 处理下载
resp, err := processDownload(&req)
resp, err := processSaveJson(&req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
@ -119,7 +131,6 @@ func saveImagesHandler(w http.ResponseWriter, r *http.Request) {
return
}
// 返回响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
@ -162,6 +173,7 @@ func downloadMissingHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Folder string `json:"folder"`
Title string `json:"title"`
WebSite string `json:"web_site"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
@ -183,7 +195,7 @@ func downloadMissingHandler(w http.ResponseWriter, r *http.Request) {
return
}
resp, err := processMissingDownload(req.Folder, req.Title)
resp, err := processMissingDownload(req.Folder, req.Title, req.WebSite)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
@ -228,32 +240,19 @@ func cleanupJsonHandler(w http.ResponseWriter, r *http.Request) {
})
}
// 查找文件夹中的JSON文件(支持多种方式)
func findJsonFiles(folderPath string) ([]string, error) {
jsonFiles := []string{}
// 方法1: 标准glob
pattern1 := filepath.Join(folderPath, "*.json")
files1, _ := filepath.Glob(pattern1)
jsonFiles = append(jsonFiles, files1...)
// 方法2: 不区分大小写
pattern2 := filepath.Join(folderPath, "*.JSON")
files2, _ := filepath.Glob(pattern2)
jsonFiles = append(jsonFiles, files2...)
// 方法3: 使用ReadDir手动查找(最可靠)
files3, err := os.ReadDir(folderPath)
files, err := os.ReadDir(folderPath)
if err != nil {
return jsonFiles, err
}
for _, file := range files3 {
for _, file := range files {
if !file.IsDir() {
name := strings.ToLower(file.Name())
if strings.HasSuffix(name, ".json") {
fullPath := filepath.Join(folderPath, file.Name())
// 去重
found := false
for _, existing := range jsonFiles {
if existing == fullPath {
@ -274,192 +273,128 @@ func findJsonFiles(folderPath string) ([]string, error) {
func loadFolders() ([]FolderInfo, error) {
folders := []FolderInfo{}
fmt.Println("=== 开始扫描下载目录 ===")
fmt.Println("下载目录绝对路径:", *downloadDir)
// 获取绝对路径
absDownloadDir, _ := filepath.Abs(*downloadDir)
fmt.Println("下载目录绝对路径:", absDownloadDir)
entries, err := os.ReadDir(absDownloadDir)
websiteEntries, err := os.ReadDir(absDownloadDir)
if err != nil {
fmt.Printf("读取目录失败: %v\n", err)
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
folderPath := filepath.Join(absDownloadDir, entry.Name())
fmt.Printf(" 处理文件夹: %s\n", folderPath)
for _, websiteEntry := range websiteEntries {
if websiteEntry.IsDir() {
websiteDir := filepath.Join(absDownloadDir, websiteEntry.Name())
// 使用通用函数查找JSON文件
jsonFiles, err := findJsonFiles(folderPath)
comicEntries, err := os.ReadDir(websiteDir)
if err != nil {
fmt.Printf(" 查找JSON文件失败: %v\n", err)
continue
}
for j, jsonFile := range jsonFiles {
fmt.Printf(" [%d] %s\n", j, jsonFile)
}
for _, comicEntry := range comicEntries {
if comicEntry.IsDir() {
folderPath := filepath.Join(websiteDir, comicEntry.Name())
if len(jsonFiles) == 0 {
fmt.Println(" 没有JSON文件,跳过")
jsonFiles, err := findJsonFiles(folderPath)
if err != nil || len(jsonFiles) == 0 {
continue
}
// 读取JSON文件
for _, jsonFile := range jsonFiles {
fmt.Printf(" 处理JSON文件: %s\n", jsonFile)
// 检查文件是否存在
if _, err := os.Stat(jsonFile); os.IsNotExist(err) {
fmt.Printf(" ❌ JSON文件不存在\n")
continue
}
data, err := os.ReadFile(jsonFile)
if err != nil {
fmt.Printf(" 读取JSON文件失败: %v\n", err)
continue
}
fmt.Printf(" 成功读取JSON文件,大小: %d bytes\n", len(data))
var jsonData JsonData
if err := json.Unmarshal(data, &jsonData); err != nil {
fmt.Printf(" 解析JSON失败: %v\n", err)
fmt.Printf(" 文件内容前100字节: %s\n", string(data[:min(100, len(data))]))
continue
}
fmt.Printf(" JSON解析成功: title='%s', imgs数量=%d\n", jsonData.Title, len(jsonData.Imgs))
if len(jsonData.Imgs) == 0 {
fmt.Println(" JSON中没有图片数据,跳过")
continue
}
// 统计实际已下载的图片数量(与JSON中的key匹配)
downloaded := 0
files, _ := os.ReadDir(folderPath)
fmt.Printf(" 文件夹中总文件数: %d\n", len(files))
// 创建已存在文件的map
existingFiles := make(map[string]bool)
for _, file := range files {
if !file.IsDir() && !strings.HasSuffix(strings.ToLower(file.Name()), ".json") {
// 提取序号(去掉扩展名)
ext := filepath.Ext(file.Name())
key := strings.TrimSuffix(file.Name(), ext)
existingFiles[key] = true
fmt.Printf(" 文件: %s -> key: %s\n", file.Name(), key)
}
}
fmt.Println(" 检查JSON中的key匹配情况:")
// 检查JSON中的每个key是否都有对应的文件
missingKeys := []string{}
for key := range jsonData.Imgs {
if existingFiles[key] {
downloaded++
fmt.Printf(" ✓ 存在: key=%s\n", key)
} else {
missingKeys = append(missingKeys, key)
fmt.Printf(" ✗ 缺失: key=%s\n", key)
}
}
// 计算进度
total := len(jsonData.Imgs)
progress := 0.0
if total > 0 {
progress = float64(downloaded) / float64(total) * 100
}
fmt.Printf(" 统计结果: 总=%d, 已下载=%d, 缺失=%d, 进度=%.2f%%\n",
total, downloaded, len(missingKeys), progress)
pending := total - downloaded
// 显示所有未完成的任务(包括部分完成的)
folders = append(folders, FolderInfo{
Title: jsonData.Title,
WebSite: jsonData.WebSiteName,
Folder: folderPath,
JsonPath: jsonFile,
Total: total,
Downloaded: downloaded,
Progress: progress,
Pending: pending,
})
fmt.Printf(" 添加到列表: title='%s', progress=%.2f%%\n", jsonData.Title, progress)
}
}
}
}
}
fmt.Printf("\n=== 扫描完成,共找到 %d 个文件夹 ===\n", len(folders))
return folders, nil
}
func processMissingDownload(folderPath, title string) (*SaveImagesResponse, error) {
fmt.Printf("=== 开始处理缺失文件下载 ===\n")
fmt.Printf("文件夹: %s\n", folderPath)
fmt.Printf("标题: %s\n", title)
// 使用通用函数查找JSON文件
func processMissingDownload(folderPath, title, webSite string) (*SaveImagesResponse, error) {
jsonFiles, err := findJsonFiles(folderPath)
if err != nil || len(jsonFiles) == 0 {
fmt.Printf("未找到JSON文件,错误: %v\n", err)
return nil, fmt.Errorf("未找到JSON文件")
}
fmt.Printf("找到JSON文件: %s\n", jsonFiles[0])
// 读取JSON文件
data, err := os.ReadFile(jsonFiles[0])
if err != nil {
fmt.Printf("读取JSON文件失败: %v\n", err)
return nil, fmt.Errorf("读取JSON文件失败: %v", err)
}
var jsonData JsonData
if err := json.Unmarshal(data, &jsonData); err != nil {
fmt.Printf("解析JSON失败: %v\n", err)
return nil, fmt.Errorf("解析JSON失败: %v", err)
}
fmt.Printf("JSON解析成功: title='%s', imgs数量=%d\n", jsonData.Title, len(jsonData.Imgs))
// 检查现有文件 - 修正为只统计与JSON key匹配的文件
existingFiles := make(map[string]bool)
files, _ := os.ReadDir(folderPath)
fmt.Printf("文件夹中总文件数: %d\n", len(files))
for _, file := range files {
if !file.IsDir() && !strings.HasSuffix(strings.ToLower(file.Name()), ".json") {
// 提取序号(去掉扩展名)
ext := filepath.Ext(file.Name())
key := strings.TrimSuffix(file.Name(), ext)
existingFiles[key] = true
}
}
// 准备下载缺失的文件
needDownload := make(map[string]string)
missingCount := 0
fmt.Println("检查缺失文件:")
for key, url := range jsonData.Imgs {
if !existingFiles[key] {
needDownload[key] = url
missingCount++
fmt.Printf(" ✗ 缺失: key=%s\n", key)
} else {
fmt.Printf(" ✓ 存在: key=%s\n", key)
}
}
// 计算已下载数量(基于JSON中实际存在的key)
downloadedCount := 0
for key := range jsonData.Imgs {
if existingFiles[key] {
@ -467,15 +402,12 @@ func processMissingDownload(folderPath, title string) (*SaveImagesResponse, erro
}
}
fmt.Printf("统计: 总文件=%d, 已下载=%d, 缺失=%d\n",
len(jsonData.Imgs), downloadedCount, missingCount)
if len(needDownload) == 0 {
fmt.Println("所有文件都已存在,无需下载")
return &SaveImagesResponse{
Success: true,
Message: "所有图片已下载完成",
Title: title,
WebSite: webSite,
Folder: folderPath,
JsonPath: jsonFiles[0],
Total: len(jsonData.Imgs),
@ -485,9 +417,6 @@ func processMissingDownload(folderPath, title string) (*SaveImagesResponse, erro
}, nil
}
fmt.Printf("开始下载 %d 个缺失文件...\n", len(needDownload))
// 下载缺失图片
saved := 0
failed := 0
details := make([]DownloadDetail, 0, len(needDownload))
@ -510,7 +439,6 @@ func processMissingDownload(folderPath, title string) (*SaveImagesResponse, erro
details[i].Status = "failed"
details[i].Message = err.Error()
failed++
fmt.Printf("下载失败: key=%s, error=%v\n", detail.Key, err)
} else {
ext := getFileExtension(detail.URL)
filename := detail.Key + ext
@ -518,19 +446,17 @@ func processMissingDownload(folderPath, title string) (*SaveImagesResponse, erro
details[i].Message = "下载成功"
details[i].SavedAs = filename
saved++
fmt.Printf("下载成功: key=%s\n", detail.Key)
}
}
}
skipped := len(jsonData.Imgs) - saved - failed
fmt.Printf("下载完成: 新增=%d, 跳过=%d, 失败=%d\n", saved, skipped, failed)
return &SaveImagesResponse{
Success: failed == 0,
Message: fmt.Sprintf("下载完成: 新增%d张, 跳过%d张, 失败%d张", saved, skipped, failed),
Title: title,
WebSite: webSite,
Folder: folderPath,
JsonPath: jsonFiles[0],
Total: len(jsonData.Imgs),
@ -541,139 +467,60 @@ func processMissingDownload(folderPath, title string) (*SaveImagesResponse, erro
}, nil
}
func cleanupJsonFiles() (int, error) {
count := 0
err := filepath.Walk(*downloadDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
func processSaveJson(req *SaveImagesRequest) (*SaveJsonResponse, error) {
cleanWebSite := "default"
if req.WebSiteName != "" {
cleanWebSite = cleanFilename(req.WebSiteName)
}
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
if err := os.Remove(path); err == nil {
count++
}
}
return nil
})
return count, err
}
func processDownload(req *SaveImagesRequest) (*SaveImagesResponse, error) {
// 清理标题(移除非法字符)
cleanTitle := cleanFilename(req.Title)
// 创建目录
comicDir := filepath.Join(*downloadDir, cleanTitle)
comicDir := filepath.Join(*downloadDir, cleanWebSite, cleanTitle)
if err := os.MkdirAll(comicDir, 0755); err != nil {
return nil, fmt.Errorf("创建目录失败: %v", err)
}
// JSON文件路径
jsonFilename := cleanTitle + ".json"
jsonPath := filepath.Join(comicDir, jsonFilename)
// 检查现有文件
existingFiles := make(map[string]bool)
files, _ := os.ReadDir(comicDir)
for _, file := range files {
if !file.IsDir() {
filename := file.Name()
if strings.HasSuffix(filename, ".json") {
continue
}
// 提取序号(去掉扩展名)
ext := filepath.Ext(filename)
key := strings.TrimSuffix(filename, ext)
existingFiles[key] = true
}
if err := saveJSON(comicDir, jsonFilename, req.Title, cleanWebSite, req.Imgs); err != nil {
return nil, fmt.Errorf("保存JSON失败: %v", err)
}
// 准备下载
details := make([]DownloadDetail, 0, len(req.Imgs))
needDownload := make(map[string]string)
for key, url := range req.Imgs {
// 获取文件扩展名
ext := getFileExtension(url)
filename := key + ext
return &SaveJsonResponse{
Success: true,
Message: "JSON文件保存成功",
Title: req.Title,
WebSite: cleanWebSite,
Folder: comicDir,
JsonPath: jsonPath,
Total: len(req.Imgs),
}, nil
}
if existingFiles[key] {
// 文件已存在,跳过
details = append(details, DownloadDetail{
Key: key,
URL: url,
Status: "skipped",
Message: "文件已存在",
SavedAs: filename,
})
continue
}
func cleanupJsonFiles() (int, error) {
count := 0
// 需要下载
needDownload[key] = url
details = append(details, DownloadDetail{
Key: key,
URL: url,
Status: "pending",
Message: "等待下载",
})
err := filepath.Walk(*downloadDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 下载图片
var saved, failed int
if len(needDownload) > 0 {
successMap, errors := downloadImages(needDownload, comicDir)
_ = successMap
// 更新下载详情
for i, detail := range details {
if detail.Status == "pending" {
if err, ok := errors[detail.Key]; ok {
details[i].Status = "failed"
details[i].Message = err.Error()
failed++
} else {
ext := getFileExtension(detail.URL)
filename := detail.Key + ext
details[i].Status = "success"
details[i].Message = "下载成功"
details[i].SavedAs = filename
saved++
}
}
}
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
if err := os.Remove(path); err == nil {
count++
}
// 保存JSON文件
skipped := len(req.Imgs) - saved - failed
finalImgs := make(map[string]string)
for key, url := range req.Imgs {
finalImgs[key] = url
}
saveJSON(comicDir, jsonFilename, req.Title, finalImgs)
return nil
})
// 构建响应
return &SaveImagesResponse{
Success: failed == 0,
Message: fmt.Sprintf("下载完成: 新增%d张, 跳过%d张, 失败%d张", saved, skipped, failed),
Title: req.Title,
Folder: comicDir,
JsonPath: jsonPath,
Total: len(req.Imgs),
Saved: saved,
Skipped: skipped,
Failed: failed,
Details: details,
}, nil
return count, err
}
func saveJSON(dir, filename, title string, imgs map[string]string) error {
func saveJSON(dir, filename, title, webSite string, imgs map[string]string) error {
data := map[string]interface{}{
"title": title,
"source": webSite,
"imgs": imgs,
"created_at": time.Now().Format("2006-01-02 15:04:05"),
"updated_at": time.Now().Format("2006-01-02 15:04:05"),
@ -701,13 +548,10 @@ func cleanFilename(filename string) string {
}
func getFileExtension(url string) string {
// 默认扩展名
ext := ".jpg"
// 从URL提取扩展名
if idx := strings.LastIndex(url, "."); idx != -1 {
fileExt := strings.ToLower(url[idx:])
// 检查是否为常见图片格式
validExts := []string{".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff"}
for _, validExt := range validExts {
if strings.HasPrefix(fileExt, validExt) {
@ -727,7 +571,6 @@ func downloadImages(images map[string]string, dir string) (map[string]bool, map[
success := make(map[string]bool)
errors := make(map[string]error)
// 并发控制:最多同时下载5张图片
semaphore := make(chan struct{}, 5)
for key, url := range images {
@ -736,11 +579,9 @@ func downloadImages(images map[string]string, dir string) (map[string]bool, map[
go func(key, url string) {
defer wg.Done()
// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()
// 下载图片
err := downloadImage(url, dir, key)
mu.Lock()
@ -758,7 +599,6 @@ func downloadImages(images map[string]string, dir string) (map[string]bool, map[
}
func downloadImage(url, dir, filename string) error {
// 创建HTTP客户端
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
@ -766,19 +606,16 @@ func downloadImage(url, dir, filename string) error {
},
}
// 创建请求
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("创建请求失败: %v", err)
}
// 设置请求头(模拟浏览器)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
req.Header.Set("Accept", "image/webp,image/apng,image/*,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Referer", "https://hd4k.com/")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("请求失败: %v", err)
@ -789,49 +626,45 @@ func downloadImage(url, dir, filename string) error {
return fmt.Errorf("HTTP错误: %s", resp.Status)
}
// 获取文件扩展名
ext := getFileExtension(url)
fullFilename := filename + ext
filePath := filepath.Join(dir, fullFilename)
// 创建文件
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("创建文件失败: %v", err)
}
defer file.Close()
// 下载并保存
_, err = io.Copy(file, resp.Body)
if err != nil {
// 删除可能已损坏的文件
os.Remove(filePath)
return fmt.Errorf("保存文件失败: %v", err)
}
// 输出保存的完整路径
absPath, _ := filepath.Abs(filePath)
fmt.Printf("✅ 图片保存成功: %s\n", absPath)
return nil
}
func main() {
flag.Parse()
// 创建下载目录
if err := os.MkdirAll(*downloadDir, 0755); err != nil {
log.Fatal("创建下载目录失败:", err)
}
// 设置路由
http.HandleFunc("/api/save_imgs", enableCORS(saveImagesHandler))
http.HandleFunc("/", enableCORS(mainHandler))
http.HandleFunc("/api/save_json", enableCORS(saveJsonHandler))
http.HandleFunc("/api/reload_folders", enableCORS(reloadFoldersHandler))
http.HandleFunc("/api/download_missing", enableCORS(downloadMissingHandler))
http.HandleFunc("/api/cleanup_json", enableCORS(cleanupJsonHandler))
// 静态文件服务
http.Handle("/", http.FileServer(http.Dir(".")))
fmt.Printf("HD4K下载服务启动\n")
fmt.Printf("下载目录: %s\n", *downloadDir)
fmt.Printf("API地址: http://127.0.0.1:%s/api/save_imgs\n", *port)
fmt.Printf("保存JSON API: http://127.0.0.1:%s/api/save_json\n", *port)
fmt.Printf("管理页面: http://127.0.0.1:%s/\n", *port)
fmt.Printf("按 Ctrl+C 停止服务\n")

File diff suppressed because it is too large Load Diff

@ -65,11 +65,21 @@ header .subtitle {
}
.btn-primary {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.3);
}
.btn-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(16, 185, 129, 0.3);
}
@ -242,170 +252,6 @@ header .subtitle {
color: #475569;
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 15px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 20px;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 1.5rem;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 1.8rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.progress-container {
background: #f8fafc;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
position: relative;
}
.progress-bar {
height: 10px;
background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
border-radius: 5px;
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
position: absolute;
top: 50%;
right: 20px;
transform: translateY(-50%);
font-weight: 600;
color: #1e293b;
}
.download-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin: 20px 0;
}
.stat-item {
background: #f8fafc;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
color: #64748b;
font-size: 0.9rem;
margin-bottom: 5px;
}
.stat-value {
display: block;
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
}
.stat-value.success {
color: #10b981;
}
.stat-value.pending {
color: #f59e0b;
}
.log-container {
background: #f8fafc;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
}
.log-container h4 {
color: #334155;
margin-bottom: 10px;
}
.log-content {
max-height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.4;
color: #475569;
}
.log-entry {
padding: 5px 0;
border-bottom: 1px solid #e2e8f0;
}
.log-entry.success {
color: #10b981;
}
.log-entry.error {
color: #ef4444;
}
.log-entry.info {
color: #3b82f6;
}
.modal-footer {
padding: 20px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
text-align: right;
}
@media (max-width: 768px) {
.container {
border-radius: 10px;
@ -439,8 +285,4 @@ header .subtitle {
.folder-actions {
justify-content: center;
}
.download-stats {
grid-template-columns: 1fr;
}
}

@ -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…
Cancel
Save