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.
 
 
 
 
 
ImagesDownloader/main.go

687 lines
17 KiB

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)
}
}