diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..f6c8e3e
--- /dev/null
+++ b/Readme.md
@@ -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
\ No newline at end of file
diff --git a/Tampermonkey/hdk4.js b/Tampermonkey/hdk4.js
new file mode 100644
index 0000000..0292282
--- /dev/null
+++ b/Tampermonkey/hdk4.js
@@ -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(`✅ 发送成功!
共 ${sortedPages.length} 页
${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下载器已加载
点击按钮开始自动翻页爬取');
+ 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();
+ }
+ }
+
+})();
\ No newline at end of file
diff --git a/hdk4_downloader.js b/hdk4_downloader.js
deleted file mode 100644
index 77c8536..0000000
--- a/hdk4_downloader.js
+++ /dev/null
@@ -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();
- }
-
-})();
\ No newline at end of file
diff --git a/index.html b/index.html
deleted file mode 100644
index 02a4d80..0000000
--- a/index.html
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
- HD4K 下载管理
-
-
-
-
-
-
- HD4K 下载管理
- 管理未完成的下载任务
-
-
-
-
-
-
-
-
-
-
-
-
API 信息
-
图片下载API: POST http://127.0.0.1:55830/api/save_imgs
-
请求格式: {"title": "漫画标题", "imgs": {"001": "url1", "002": "url2"}}
-
-
-
-
-
-
-
-
-
-
-
- 总文件数:
- 0
-
-
- 已下载:
- 0
-
-
- 待下载:
- 0
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/index.js b/index.js
deleted file mode 100644
index 6b15dcb..0000000
--- a/index.js
+++ /dev/null
@@ -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 => `
-
-
-
-
- ${this.escapeHtml(folder.title)}
-
-
- 总文件: ${folder.total}
- 已下载: ${folder.downloaded}
- 缺失: ${folder.total - folder.downloaded}
-
-
-
-
-
${folder.progress.toFixed(1)}%
-
-
-
-
-
-
-
- `).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();
\ No newline at end of file
diff --git a/main.go b/main.go
index 9cceaf9..b70b66e 100644
--- a/main.go
+++ b/main.go
@@ -21,8 +21,9 @@ var (
)
type SaveImagesRequest struct {
- Title string `json:"title"`
- Imgs map[string]string `json:"imgs"`
+ 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"`
- Imgs map[string]string `json:"imgs"`
+ 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)
}
@@ -160,8 +171,9 @@ func downloadMissingHandler(w http.ResponseWriter, r *http.Request) {
}
var req struct {
- Folder string `json:"folder"`
- Title string `json:"title"`
+ 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)
- }
-
- if len(jsonFiles) == 0 {
- fmt.Println(" 没有JSON文件,跳过")
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
- }
+ for _, comicEntry := range comicEntries {
+ if comicEntry.IsDir() {
+ folderPath := filepath.Join(websiteDir, comicEntry.Name())
- // 统计实际已下载的图片数量(与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)
+ jsonFiles, err := findJsonFiles(folderPath)
+ if err != nil || len(jsonFiles) == 0 {
+ continue
}
- }
- 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)
+ for _, jsonFile := range jsonFiles {
+ data, err := os.ReadFile(jsonFile)
+ if err != nil {
+ continue
+ }
+
+ var jsonData JsonData
+ if err := json.Unmarshal(data, &jsonData); err != nil {
+ continue
+ }
+
+ if len(jsonData.Imgs) == 0 {
+ continue
+ }
+
+ downloaded := 0
+ files, _ := os.ReadDir(folderPath)
+
+ 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
+ }
+ }
+
+ for key := range jsonData.Imgs {
+ if existingFiles[key] {
+ downloaded++
+ }
+ }
+
+ total := len(jsonData.Imgs)
+ progress := 0.0
+ if total > 0 {
+ progress = float64(downloaded) / float64(total) * 100
+ }
+
+ 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,
+ })
}
}
-
- // 计算进度
- 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)
-
- // 显示所有未完成的任务(包括部分完成的)
- folders = append(folders, FolderInfo{
- Title: jsonData.Title,
- Folder: folderPath,
- JsonPath: jsonFile,
- Total: total,
- Downloaded: downloaded,
- Progress: progress,
- })
-
- 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
- }
-
- 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) {
- // 清理标题(移除非法字符)
+func processSaveJson(req *SaveImagesRequest) (*SaveJsonResponse, error) {
+ cleanWebSite := "default"
+ if req.WebSiteName != "" {
+ cleanWebSite = cleanFilename(req.WebSiteName)
+ }
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)
+ return &SaveJsonResponse{
+ Success: true,
+ Message: "JSON文件保存成功",
+ Title: req.Title,
+ WebSite: cleanWebSite,
+ Folder: comicDir,
+ JsonPath: jsonPath,
+ Total: len(req.Imgs),
+ }, nil
+}
- for key, url := range req.Imgs {
- // 获取文件扩展名
- ext := getFileExtension(url)
- filename := key + ext
+func cleanupJsonFiles() (int, error) {
+ count := 0
- if existingFiles[key] {
- // 文件已存在,跳过
- details = append(details, DownloadDetail{
- Key: key,
- URL: url,
- Status: "skipped",
- Message: "文件已存在",
- SavedAs: filename,
- })
- continue
+ err := filepath.Walk(*downloadDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
}
- // 需要下载
- needDownload[key] = url
- details = append(details, DownloadDetail{
- Key: key,
- URL: url,
- Status: "pending",
- Message: "等待下载",
- })
- }
-
- // 下载图片
- 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")
diff --git a/prompt.txt b/prompt.txt
new file mode 100644
index 0000000..9221c3a
--- /dev/null
+++ b/prompt.txt
@@ -0,0 +1,1702 @@
+.
+├── Tampermonkey
+│ └── hdk4.js
+├── build
+│ ├── hd4k-downloader-macos-arm
+│ ├── hd4k-downloader-macos-intel
+│ └── hd4k-downloader-windows.exe
+├── build.sh
+├── downloads
+├── go.mod
+├── index.css
+├── index.html
+├── index.js
+├── main.go
+├── prompt.txt
+└── test.txt
+
+
+
+---
+main.go
+
+package main
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+)
+
+var (
+ downloadDir = flag.String("dir", "./downloads", "下载目录")
+ port = flag.String("port", "55830", "服务端口")
+)
+
+type SaveImagesRequest struct {
+ Title string `json:"title"`
+ Imgs map[string]string `json:"imgs"`
+}
+
+// 下载详情
+type DownloadDetail struct {
+ Key string `json:"key"`
+ URL string `json:"url"`
+ Status string `json:"status"` // success, skipped, failed
+ Message string `json:"message"`
+ SavedAs string `json:"saved_as,omitempty"`
+}
+
+// 响应结构
+type SaveImagesResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ Title string `json:"title"`
+ Folder string `json:"folder"`
+ JsonPath string `json:"json_path"`
+ Total int `json:"total"`
+ Saved int `json:"saved"`
+ Skipped int `json:"skipped"`
+ Failed int `json:"failed"`
+ Details []DownloadDetail `json:"details"`
+}
+
+// 文件夹信息结构
+type FolderInfo struct {
+ Title string `json:"title"`
+ Folder string `json:"folder"`
+ JsonPath string `json:"json_path"`
+ Total int `json:"total"`
+ Downloaded int `json:"downloaded"`
+ Progress float64 `json:"progress"`
+}
+
+// 加载JSON数据
+type JsonData struct {
+ Title string `json:"title"`
+ Imgs map[string]string `json:"imgs"`
+}
+
+func saveImagesHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "只支持POST请求",
+ })
+ return
+ }
+
+ // 解析请求
+ var req SaveImagesRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "请求格式错误: " + err.Error(),
+ })
+ return
+ }
+
+ // 验证参数
+ if req.Title == "" {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "标题不能为空",
+ })
+ 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)
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "处理失败: " + err.Error(),
+ })
+ return
+ }
+
+ // 返回响应
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+}
+
+func reloadFoldersHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "只支持GET请求",
+ })
+ return
+ }
+
+ folders, err := loadFolders()
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "加载文件夹失败: " + err.Error(),
+ })
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": true,
+ "folders": folders,
+ })
+}
+
+func downloadMissingHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, `{"success": false, "message": "只支持POST请求"}`, http.StatusMethodNotAllowed)
+ return
+ }
+
+ var req struct {
+ Folder string `json:"folder"`
+ Title string `json:"title"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "请求格式错误: " + err.Error(),
+ })
+ return
+ }
+
+ if req.Folder == "" || req.Title == "" {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "参数不能为空",
+ })
+ return
+ }
+
+ resp, err := processMissingDownload(req.Folder, req.Title)
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "处理失败: " + err.Error(),
+ })
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+}
+
+func cleanupJsonHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "只支持POST请求",
+ })
+ return
+ }
+
+ count, err := cleanupJsonFiles()
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "清理失败: " + err.Error(),
+ })
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": true,
+ "message": fmt.Sprintf("已删除 %d 个JSON文件", count),
+ "deleted": count,
+ })
+}
+
+// 查找文件夹中的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)
+ if err != nil {
+ return jsonFiles, err
+ }
+
+ for _, file := range files3 {
+ 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 {
+ found = true
+ break
+ }
+ }
+ if !found {
+ jsonFiles = append(jsonFiles, fullPath)
+ }
+ }
+ }
+ }
+
+ return jsonFiles, nil
+}
+
+func loadFolders() ([]FolderInfo, error) {
+ folders := []FolderInfo{}
+
+ fmt.Println("=== 开始扫描下载目录 ===")
+ fmt.Println("下载目录绝对路径:", *downloadDir)
+
+ // 获取绝对路径
+ absDownloadDir, _ := filepath.Abs(*downloadDir)
+ fmt.Println("下载目录绝对路径:", absDownloadDir)
+
+ entries, 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)
+
+ // 使用通用函数查找JSON文件
+ jsonFiles, err := findJsonFiles(folderPath)
+ if err != nil {
+ fmt.Printf(" 查找JSON文件失败: %v\n", err)
+ continue
+ }
+
+ for j, jsonFile := range jsonFiles {
+ fmt.Printf(" [%d] %s\n", j, jsonFile)
+ }
+
+ if len(jsonFiles) == 0 {
+ fmt.Println(" 没有JSON文件,跳过")
+ 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)
+
+ // 显示所有未完成的任务(包括部分完成的)
+ folders = append(folders, FolderInfo{
+ Title: jsonData.Title,
+ Folder: folderPath,
+ JsonPath: jsonFile,
+ Total: total,
+ Downloaded: downloaded,
+ Progress: progress,
+ })
+
+ 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文件
+ 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] {
+ downloadedCount++
+ }
+ }
+
+ fmt.Printf("统计: 总文件=%d, 已下载=%d, 缺失=%d\n",
+ len(jsonData.Imgs), downloadedCount, missingCount)
+
+ if len(needDownload) == 0 {
+ fmt.Println("所有文件都已存在,无需下载")
+ return &SaveImagesResponse{
+ Success: true,
+ Message: "所有图片已下载完成",
+ Title: title,
+ Folder: folderPath,
+ JsonPath: jsonFiles[0],
+ Total: len(jsonData.Imgs),
+ Saved: 0,
+ Skipped: len(jsonData.Imgs),
+ Failed: 0,
+ }, nil
+ }
+
+ fmt.Printf("开始下载 %d 个缺失文件...\n", len(needDownload))
+
+ // 下载缺失图片
+ saved := 0
+ failed := 0
+ details := make([]DownloadDetail, 0, len(needDownload))
+
+ for key, url := range needDownload {
+ details = append(details, DownloadDetail{
+ Key: key,
+ URL: url,
+ Status: "pending",
+ Message: "等待下载",
+ })
+ }
+
+ successMap, errors := downloadImages(needDownload, folderPath)
+ _ = 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++
+ fmt.Printf("下载失败: key=%s, error=%v\n", detail.Key, err)
+ } else {
+ ext := getFileExtension(detail.URL)
+ filename := detail.Key + ext
+ details[i].Status = "success"
+ 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,
+ Folder: folderPath,
+ JsonPath: jsonFiles[0],
+ Total: len(jsonData.Imgs),
+ Saved: saved,
+ Skipped: skipped,
+ Failed: failed,
+ Details: details,
+ }, 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
+ }
+
+ 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)
+ 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
+ }
+ }
+
+ // 准备下载
+ details := make([]DownloadDetail, 0, len(req.Imgs))
+ needDownload := make(map[string]string)
+
+ for key, url := range req.Imgs {
+ // 获取文件扩展名
+ ext := getFileExtension(url)
+ filename := key + ext
+
+ if existingFiles[key] {
+ // 文件已存在,跳过
+ details = append(details, DownloadDetail{
+ Key: key,
+ URL: url,
+ Status: "skipped",
+ Message: "文件已存在",
+ SavedAs: filename,
+ })
+ continue
+ }
+
+ // 需要下载
+ needDownload[key] = url
+ details = append(details, DownloadDetail{
+ Key: key,
+ URL: url,
+ Status: "pending",
+ Message: "等待下载",
+ })
+ }
+
+ // 下载图片
+ 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++
+ }
+ }
+ }
+ }
+
+ // 保存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 &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
+}
+
+func saveJSON(dir, filename, title string, imgs map[string]string) error {
+ data := map[string]interface{}{
+ "title": title,
+ "imgs": imgs,
+ "created_at": time.Now().Format("2006-01-02 15:04:05"),
+ "updated_at": time.Now().Format("2006-01-02 15:04:05"),
+ }
+
+ jsonData, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ return err
+ }
+
+ path := filepath.Join(dir, filename)
+ return os.WriteFile(path, jsonData, 0644)
+}
+
+func cleanFilename(filename string) string {
+ illegalChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "\n", "\r", "\t"}
+ result := filename
+ for _, char := range illegalChars {
+ result = strings.ReplaceAll(result, char, "_")
+ }
+ if len(result) > 200 {
+ result = result[:200]
+ }
+ return strings.TrimSpace(result)
+}
+
+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) {
+ ext = validExt
+ break
+ }
+ }
+ }
+
+ return ext
+}
+
+func downloadImages(images map[string]string, dir string) (map[string]bool, map[string]error) {
+ var mu sync.Mutex
+ var wg sync.WaitGroup
+
+ success := make(map[string]bool)
+ errors := make(map[string]error)
+
+ // 并发控制:最多同时下载5张图片
+ semaphore := make(chan struct{}, 5)
+
+ for key, url := range images {
+ wg.Add(1)
+
+ go func(key, url string) {
+ defer wg.Done()
+
+ // 获取信号量
+ semaphore <- struct{}{}
+ defer func() { <-semaphore }()
+
+ // 下载图片
+ err := downloadImage(url, dir, key)
+
+ mu.Lock()
+ if err != nil {
+ errors[key] = err
+ } else {
+ success[key] = true
+ }
+ mu.Unlock()
+ }(key, url)
+ }
+
+ wg.Wait()
+ return success, errors
+}
+
+func downloadImage(url, dir, filename string) error {
+ // 创建HTTP客户端
+ client := &http.Client{
+ Timeout: 30 * time.Second,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+
+ // 创建请求
+ 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)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ 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)
+ }
+
+ 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("/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("管理页面: http://127.0.0.1:%s/\n", *port)
+ fmt.Printf("按 Ctrl+C 停止服务\n")
+
+ log.Fatal(http.ListenAndServe(":"+*port, nil))
+}
+
+func enableCORS(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ next(w, r)
+ }
+}
+
+
+---
+
+index.html
+
+
+
+
+
+
+ HD4K 下载管理
+
+
+
+
+
+
+ HD4K 下载管理
+ 管理未完成的下载任务
+
+
+
+
+
+
+
+
+
+
+
+
API 信息
+
图片下载API: POST http://127.0.0.1:55830/api/save_imgs
+
请求格式: {"title": "漫画标题", "imgs": {"001": "url1", "002": "url2"}}
+
+
+
+
+
+
+
+
+
+
+
+ 总文件数:
+ 0
+
+
+ 已下载:
+ 0
+
+
+ 待下载:
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+---
+index.css
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ padding: 20px;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ background: white;
+ border-radius: 20px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ overflow: hidden;
+}
+
+header {
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+ color: white;
+ padding: 40px;
+ text-align: center;
+}
+
+header h1 {
+ font-size: 2.5rem;
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 15px;
+}
+
+header .subtitle {
+ font-size: 1.1rem;
+ opacity: 0.9;
+}
+
+.control-panel {
+ padding: 30px;
+ background: #f8fafc;
+ border-bottom: 1px solid #e2e8f0;
+ display: flex;
+ gap: 15px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.btn {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 10px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ transition: all 0.3s ease;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ color: white;
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(16, 185, 129, 0.3);
+}
+
+.btn-warning {
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
+ color: white;
+}
+
+.btn-warning:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(245, 158, 11, 0.3);
+}
+
+.btn-secondary {
+ background: #64748b;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #475569;
+}
+
+.btn-small {
+ padding: 8px 16px;
+ font-size: 0.9rem;
+}
+
+.status {
+ margin-left: auto;
+ padding: 10px 20px;
+ background: white;
+ border-radius: 8px;
+ border-left: 4px solid #10b981;
+ font-weight: 500;
+}
+
+.folders-container {
+ padding: 30px;
+}
+
+.folders-container h2 {
+ color: #334155;
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.folders-list {
+ display: grid;
+ gap: 20px;
+}
+
+.folder-item {
+ background: white;
+ border: 2px solid #e2e8f0;
+ border-radius: 12px;
+ padding: 20px;
+ transition: all 0.3s ease;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.folder-item:hover {
+ border-color: #94a3b8;
+ transform: translateY(-2px);
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+}
+
+.folder-info {
+ flex: 1;
+}
+
+.folder-title {
+ font-size: 1.2rem;
+ font-weight: 600;
+ color: #1e293b;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.folder-stats {
+ display: flex;
+ gap: 20px;
+ color: #64748b;
+ font-size: 0.9rem;
+}
+
+.progress-container {
+ width: 200px;
+ margin: 10px 0;
+}
+
+.progress-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.progress {
+ flex: 1;
+ height: 8px;
+ background: #e2e8f0;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+}
+
+.progress-text {
+ font-weight: 600;
+ color: #1e293b;
+ min-width: 50px;
+}
+
+.folder-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.no-folders {
+ text-align: center;
+ padding: 60px 20px;
+ color: #64748b;
+}
+
+.no-folders i {
+ font-size: 4rem;
+ color: #10b981;
+ margin-bottom: 20px;
+}
+
+.no-folders p {
+ font-size: 1.2rem;
+}
+
+.api-info {
+ padding: 30px;
+ background: #f1f5f9;
+ border-top: 1px solid #e2e8f0;
+}
+
+.api-info h3 {
+ color: #334155;
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.api-info code {
+ background: #e2e8f0;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9rem;
+}
+
+.api-info p {
+ margin: 10px 0;
+ 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;
+ }
+
+ header {
+ padding: 30px 20px;
+ }
+
+ header h1 {
+ font-size: 2rem;
+ }
+
+ .control-panel {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .status {
+ margin-left: 0;
+ margin-top: 10px;
+ text-align: center;
+ }
+
+ .folder-item {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 15px;
+ }
+
+ .folder-actions {
+ justify-content: center;
+ }
+
+ .download-stats {
+ grid-template-columns: 1fr;
+ }
+}
+
+---
+index.js
+
+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 => `
+
+
+
+
+ ${this.escapeHtml(folder.title)}
+
+
+ 总文件: ${folder.total}
+ 已下载: ${folder.downloaded}
+ 缺失: ${folder.total - folder.downloaded}
+
+
+
+
+
${folder.progress.toFixed(1)}%
+
+
+
+
+
+
+
+ `).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();
+
+
+---
+
+帮我修改一下这份代码, 现在打开项目的页面路由, 会出现类似文件服务器的界面, 就是把我项目所有的文件都显示出来了, 就是显示不了 html的页面
+你帮我看看什么问题?
+另外, 前端推送过来的数据格式是
+{
+"title": "测试",
+"imgs": {
+"0001": "https://icon-icons.com/images/menu_photos.png",
+"0002": "https://icon-icons.com/images/flags/zh.webp"
+}
+}
+
+现在前端推送过来的数据变成了
+{
+"title": "测试",
+"source": "hd4k"
+"imgs": {
+"0001": "https://icon-icons.com/images/menu_photos.png",
+"0002": "https://icon-icons.com/images/flags/zh.webp"
+}
+}
+
+这个 source , 是用于, 创建文件夹的, 现在的下载文件的文件夹路径是 ./downloads/title的值, 我需要改成 ./downloads/source的值/title的值
+如果source的值不存在则创建一个
diff --git a/index.css b/src/index.css
similarity index 63%
rename from index.css
rename to src/index.css
index 8f1e1ce..a5da674 100644
--- a/index.css
+++ b/src/index.css
@@ -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;
- }
}
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..9909ce0
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+ HD4K 下载管理
+
+
+
+
+
+
+ HD4K 下载管理
+ 管理未完成的下载任务
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..9669d76
--- /dev/null
+++ b/src/index.js
@@ -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 => `
+
+
+
+
+ ${this.escapeHtml(folder.title || '未命名')}
+
+ (${folder.web_site || 'default'})
+
+
+
+ 总文件: ${folder.total || 0}
+ 已下载: ${folder.downloaded || 0}
+ 待下载: ${folder.pending || 0}
+
+
+
+
+
${(folder.progress || 0).toFixed(1)}%
+
+
+
+
+ `).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();
\ No newline at end of file