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 - - - - - - - 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/test.txt b/test.txt deleted file mode 100644 index 040cafb..0000000 --- a/test.txt +++ /dev/null @@ -1,9 +0,0 @@ -curl -X POST http://127.0.0.1:8888/api/save_imgs \ --H "Content-Type: application/json" \ --d '{ -"title": "测试", -"imgs": { -"0001": "https://icon-icons.com/images/menu_photos.png", -"0002": "https://icon-icons.com/images/flags/zh.webp" -} -}' \ No newline at end of file