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"` WebSiteName string `json:"source"` 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"` WebSite string `json:"web_site"` 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"` WebSite string `json:"web_site"` Folder string `json:"folder"` JsonPath string `json:"json_path"` Total int `json:"total"` Downloaded int `json:"downloaded"` Progress float64 `json:"progress"` Pending int `json:"pending"` } // 加载JSON数据 type JsonData struct { Title string `json:"title"` WebSiteName string `json:"source"` Imgs map[string]string `json:"imgs"` } 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) 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 } resp, err := processSaveJson(&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"` WebSite string `json:"web_site"` } 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, req.WebSite) 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, }) } func findJsonFiles(folderPath string) ([]string, error) { jsonFiles := []string{} files, err := os.ReadDir(folderPath) if err != nil { return jsonFiles, err } 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 { found = true break } } if !found { jsonFiles = append(jsonFiles, fullPath) } } } } return jsonFiles, nil } func loadFolders() ([]FolderInfo, error) { folders := []FolderInfo{} absDownloadDir, _ := filepath.Abs(*downloadDir) websiteEntries, err := os.ReadDir(absDownloadDir) if err != nil { return nil, err } for _, websiteEntry := range websiteEntries { if websiteEntry.IsDir() { websiteDir := filepath.Join(absDownloadDir, websiteEntry.Name()) comicEntries, err := os.ReadDir(websiteDir) if err != nil { continue } for _, comicEntry := range comicEntries { if comicEntry.IsDir() { folderPath := filepath.Join(websiteDir, comicEntry.Name()) jsonFiles, err := findJsonFiles(folderPath) if err != nil || len(jsonFiles) == 0 { continue } 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, }) } } } } } return folders, nil } func processMissingDownload(folderPath, title, webSite string) (*SaveImagesResponse, error) { jsonFiles, err := findJsonFiles(folderPath) if err != nil || len(jsonFiles) == 0 { return nil, fmt.Errorf("未找到JSON文件") } data, err := os.ReadFile(jsonFiles[0]) if err != nil { return nil, fmt.Errorf("读取JSON文件失败: %v", err) } var jsonData JsonData if err := json.Unmarshal(data, &jsonData); err != nil { return nil, fmt.Errorf("解析JSON失败: %v", err) } existingFiles := make(map[string]bool) files, _ := os.ReadDir(folderPath) 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 for key, url := range jsonData.Imgs { if !existingFiles[key] { needDownload[key] = url missingCount++ } } downloadedCount := 0 for key := range jsonData.Imgs { if existingFiles[key] { downloadedCount++ } } if len(needDownload) == 0 { return &SaveImagesResponse{ Success: true, Message: "所有图片已下载完成", Title: title, WebSite: webSite, Folder: folderPath, JsonPath: jsonFiles[0], Total: len(jsonData.Imgs), Saved: 0, Skipped: len(jsonData.Imgs), Failed: 0, }, nil } 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++ } else { ext := getFileExtension(detail.URL) filename := detail.Key + ext details[i].Status = "success" details[i].Message = "下载成功" details[i].SavedAs = filename saved++ } } } skipped := len(jsonData.Imgs) - saved - 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), Saved: saved, Skipped: skipped, Failed: failed, Details: details, }, nil } func processSaveJson(req *SaveImagesRequest) (*SaveJsonResponse, error) { cleanWebSite := "default" if req.WebSiteName != "" { cleanWebSite = cleanFilename(req.WebSiteName) } cleanTitle := cleanFilename(req.Title) comicDir := filepath.Join(*downloadDir, cleanWebSite, cleanTitle) if err := os.MkdirAll(comicDir, 0755); err != nil { return nil, fmt.Errorf("创建目录失败: %v", err) } jsonFilename := cleanTitle + ".json" jsonPath := filepath.Join(comicDir, jsonFilename) if err := saveJSON(comicDir, jsonFilename, req.Title, cleanWebSite, req.Imgs); err != nil { return nil, fmt.Errorf("保存JSON失败: %v", err) } return &SaveJsonResponse{ Success: true, Message: "JSON文件保存成功", Title: req.Title, WebSite: cleanWebSite, Folder: comicDir, JsonPath: jsonPath, Total: len(req.Imgs), }, 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 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"), } 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" 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) 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 { 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) } // 输出保存的完整路径 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("/", 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)) fmt.Printf("HD4K下载服务启动\n") fmt.Printf("下载目录: %s\n", *downloadDir) 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") 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) } }