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.
687 lines
17 KiB
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("Images下载服务启动\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)
|
|
}
|
|
}
|
|
|