You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
5.8 KiB
237 lines
5.8 KiB
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
|
|
}
|
|
|