diff --git a/downloader.go b/downloader.go deleted file mode 100644 index f11a1cd..0000000 --- a/downloader.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "crypto/tls" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "sync" - "time" -) - -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 -} diff --git a/handler.go b/handler.go deleted file mode 100644 index 63d8d8c..0000000 --- a/handler.go +++ /dev/null @@ -1,237 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "time" -) - -// 请求结构 -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"` -} - -func saveImagesHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "只支持POST请求", http.StatusMethodNotAllowed) - return - } - - // 解析请求 - var req SaveImagesRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "请求格式错误: "+err.Error(), http.StatusBadRequest) - return - } - - // 验证参数 - if req.Title == "" { - http.Error(w, "标题不能为空", http.StatusBadRequest) - return - } - if len(req.Imgs) == 0 { - http.Error(w, "图片列表不能为空", http.StatusBadRequest) - return - } - - // 处理下载 - resp, err := processDownload(&req) - if err != nil { - http.Error(w, "处理失败: "+err.Error(), http.StatusInternalServerError) - return - } - - // 返回响应 - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) -} - -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 来检查哪些下载成功 - // 虽然我们不直接使用 successMap,但它用于 downloadImages 函数的返回值 - _ = 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 -} diff --git a/index.css b/index.css new file mode 100644 index 0000000..8f1e1ce --- /dev/null +++ b/index.css @@ -0,0 +1,446 @@ +* { + 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; + } +} \ No newline at end of file diff --git a/index.html b/index.html index a2eebf6..02a4d80 100644 --- a/index.html +++ b/index.html @@ -1,181 +1,84 @@ - +
-管理未完成的下载任务
+所有下载任务已完成!
+图片下载API: POST http://127.0.0.1:55830/api/save_imgs
请求格式: {"title": "漫画标题", "imgs": {"001": "url1", "002": "url2"}}