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"}}

-
-
- - - - - - - \ 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"}}

+
+
+ + + + + + + + +--- +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