diff --git a/build.sh b/build.sh index 21d7465..31c8a2b 100644 --- a/build.sh +++ b/build.sh @@ -1,106 +1,72 @@ #!/bin/bash # HD4K下载器 - 跨平台构建脚本 -# 构建 Windows 和 macOS 版本到 build 文件夹 - -echo "========================================" echo "HD4K下载器 - 跨平台构建" -echo "========================================" # 设置变量 -APP_NAME="hd4k-downloader" -VERSION="1.0.0" BUILD_DIR="./build" +SRC_DIR="./src" -echo "创建构建目录..." -# 创建build目录(如果不存在) -mkdir -p "${BUILD_DIR}" +# 检查Go环境 +if ! command -v go &> /dev/null; then + echo "错误: 未安装 Go" + exit 1 +fi -echo "清理旧文件..." -# 清理build目录中的旧文件 -rm -f "${BUILD_DIR}/${APP_NAME}-windows.exe" -rm -f "${BUILD_DIR}/${APP_NAME}-macos" -rm -f "${BUILD_DIR}/${APP_NAME}-macos-arm64" +echo "Go版本: $(go version)" -echo "" -echo "开始构建 Windows 版本 (amd64)..." -echo "----------------------------------------" -GOOS=windows GOARCH=amd64 go build -o "${BUILD_DIR}/${APP_NAME}-windows.exe" main.go handler.go downloader.go +# 清理并创建目录 +echo "准备目录..." +rm -rf "${BUILD_DIR}" +mkdir -p "${BUILD_DIR}" -if [ $? -eq 0 ]; then - echo "✅ Windows 版本构建成功" - echo " 文件: ${BUILD_DIR}/${APP_NAME}-windows.exe" - echo " 大小:" $(ls -lh "${BUILD_DIR}/${APP_NAME}-windows.exe" | awk '{print $5}') +# 复制整个src文件夹 +echo "复制前端文件..." +if [ -d "${SRC_DIR}" ]; then + cp -r "${SRC_DIR}" "${BUILD_DIR}/" + echo "✅ 复制 src 文件夹" else - echo "❌ Windows 版本构建失败" + echo "❌ 找不到 src 文件夹" + exit 1 fi -echo "" -echo "开始构建 macOS 版本 (Intel amd64)..." -echo "----------------------------------------" -GOOS=darwin GOARCH=amd64 go build -o "${BUILD_DIR}/${APP_NAME}-macos-intel" main.go handler.go downloader.go - +# 构建Windows版本 +echo "构建Windows版本..." +GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o "${BUILD_DIR}/hd4k-downloader-windows.exe" main.go if [ $? -eq 0 ]; then - echo "✅ macOS Intel 版本构建成功" - echo " 文件: ${BUILD_DIR}/${APP_NAME}-macos-intel" - echo " 大小:" $(ls -lh "${BUILD_DIR}/${APP_NAME}-macos-intel" | awk '{print $5}') - chmod +x "${BUILD_DIR}/${APP_NAME}-macos-intel" + echo "✅ Windows版本构建成功" else - echo "❌ macOS Intel 版本构建失败" + echo "❌ Windows版本构建失败" fi -echo "" -echo "开始构建 macOS 版本 (Apple Silicon arm64)..." -echo "----------------------------------------" -GOOS=darwin GOARCH=arm64 go build -o "${BUILD_DIR}/${APP_NAME}-macos-arm" main.go handler.go downloader.go +# 构建macOS Intel版本 +echo "构建macOS Intel版本..." +GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o "${BUILD_DIR}/hd4k-downloader-macos-intel" main.go +if [ $? -eq 0 ]; then + echo "✅ macOS Intel版本构建成功" + chmod +x "${BUILD_DIR}/hd4k-downloader-macos-intel" +else + echo "❌ macOS Intel版本构建失败" +fi +# 构建macOS ARM版本 +echo "构建macOS ARM版本..." +GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o "${BUILD_DIR}/hd4k-downloader-macos-arm" main.go if [ $? -eq 0 ]; then - echo "✅ macOS Apple Silicon 版本构建成功" - echo " 文件: ${BUILD_DIR}/${APP_NAME}-macos-arm" - echo " 大小:" $(ls -lh "${BUILD_DIR}/${APP_NAME}-macos-arm" | awk '{print $5}') - chmod +x "${BUILD_DIR}/${APP_NAME}-macos-arm" + echo "✅ macOS ARM版本构建成功" + chmod +x "${BUILD_DIR}/hd4k-downloader-macos-arm" else - echo "❌ macOS Apple Silicon 版本构建失败" + echo "❌ macOS ARM版本构建失败" fi echo "" -echo "========================================" echo "构建完成!" -echo "========================================" -echo "" echo "生成的文件在 build/ 目录:" -echo "----------------------------------------" -ls -lh "${BUILD_DIR}/" | grep -v "^total" -echo "" - -echo "使用方法:" -echo "----------------------------------------" -echo "" -echo "Windows:" -echo " 双击 ${APP_NAME}-windows.exe" -echo " 或命令行: ${BUILD_DIR}/${APP_NAME}-windows.exe -port=8888 -dir=./downloads" -echo "" -echo "macOS (Intel芯片):" -echo " 终端执行: ${BUILD_DIR}/${APP_NAME}-macos-intel -port=8888 -dir=./downloads" -echo "" -echo "macOS (Apple Silicon M1/M2/M3芯片):" -echo " 终端执行: ${BUILD_DIR}/${APP_NAME}-macos-arm -port=8888 -dir=./downloads" -echo "" -echo "常用参数:" -echo " -port=端口号 指定服务端口(默认: 8888)" -echo " -dir=目录路径 指定下载目录(默认: ./downloads)" -echo "" -echo "示例:" -echo " # 使用自定义端口" -echo " ${BUILD_DIR}/${APP_NAME}-macos-intel -port=9999" -echo "" -echo " # 使用自定义下载目录" -echo " ${BUILD_DIR}/${APP_NAME}-windows.exe -dir=D:\\下载" -echo "" -echo " # 完整示例" -echo " ${BUILD_DIR}/${APP_NAME}-macos-arm -port=9000 -dir=~/Downloads/" -echo "" -echo "服务启动后访问:" -echo " API接口: http://127.0.0.1:端口号/api/save_imgs" -echo " 测试页面: http://127.0.0.1:端口号/index.html" -echo "========================================" \ No newline at end of file +find "${BUILD_DIR}" -type f | while read file; do + rel_path="${file#${BUILD_DIR}/}" + if [[ "$file" == *.exe ]] || [[ "$file" == *-macos-* ]]; then + echo "📦 $rel_path" + else + echo " $rel_path" + fi +done \ No newline at end of file diff --git a/prompt.txt b/prompt.txt deleted file mode 100644 index 9221c3a..0000000 --- a/prompt.txt +++ /dev/null @@ -1,1702 +0,0 @@ -. -├── 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 - - - -
- - -管理未完成的下载任务
-所有下载任务已完成!
-图片下载API: POST http://127.0.0.1:55830/api/save_imgs
请求格式: {"title": "漫画标题", "imgs": {"001": "url1", "002": "url2"}}