commit
defbba01fa
@ -0,0 +1,421 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"bufio" |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"fyne.io/fyne/v2/widget" |
||||
"golang.org/x/net/html" |
||||
"golang.org/x/sync/semaphore" |
||||
) |
||||
|
||||
// -------------------- 全局变量 --------------------
|
||||
var ( |
||||
illegalChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`) |
||||
imgLinkRegex = regexp.MustCompile(`<a href="(.*?)"`) |
||||
) |
||||
|
||||
// -------------------- 数据结构 --------------------
|
||||
type GalleryResult struct { |
||||
URL string |
||||
OK bool |
||||
Error error |
||||
} |
||||
|
||||
type CrawlStats struct { |
||||
Total int |
||||
Success int |
||||
Failed int |
||||
Skipped int |
||||
mu sync.Mutex |
||||
} |
||||
|
||||
// -------------------- 工具函数 --------------------
|
||||
func cleanFolderName(title string) string { |
||||
// 移除非法字符
|
||||
clean := illegalChars.ReplaceAllString(title, "_") |
||||
// 移除空格和下划线
|
||||
clean = strings.ReplaceAll(clean, " ", "") |
||||
clean = strings.ReplaceAll(clean, "_", "") |
||||
clean = strings.TrimSpace(clean) |
||||
|
||||
if clean == "" { |
||||
return "gallery" |
||||
} |
||||
return clean |
||||
} |
||||
|
||||
func loadTargets() ([]string, error) { |
||||
file, err := os.Open(TargetsFile) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
// 创建空文件
|
||||
file, err := os.Create(TargetsFile) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("创建目标文件失败: %v", err) |
||||
} |
||||
file.Close() |
||||
return nil, fmt.Errorf("目标文件不存在,已自动创建,请先填写URL") |
||||
} |
||||
return nil, err |
||||
} |
||||
defer file.Close() |
||||
|
||||
var targets []string |
||||
seen := make(map[string]bool) |
||||
scanner := bufio.NewScanner(file) |
||||
for scanner.Scan() { |
||||
line := strings.TrimSpace(scanner.Text()) |
||||
if line != "" && !seen[line] { |
||||
targets = append(targets, line) |
||||
seen[line] = true |
||||
} |
||||
} |
||||
|
||||
if err := scanner.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if len(targets) == 0 { |
||||
return nil, fmt.Errorf("目标文件为空,请先填写URL") |
||||
} |
||||
|
||||
return targets, nil |
||||
} |
||||
|
||||
func loadFailedUrl() ([]string, error) { |
||||
data, err := os.ReadFile(FailedRecordUrl) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return []string{}, nil |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
var failed []string |
||||
err = json.Unmarshal(data, &failed) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return failed, nil |
||||
} |
||||
|
||||
func saveFailedUrl(keys []string) error { |
||||
data, err := json.MarshalIndent(keys, "", " ") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return os.WriteFile(FailedRecordUrl, data, 0644) |
||||
} |
||||
|
||||
func ensureDownloadsDir() error { |
||||
return os.MkdirAll(DownloadsDir, 0755) |
||||
} |
||||
|
||||
func fetchPage(client *http.Client, url string) (string, error) { |
||||
var lastErr error |
||||
|
||||
for attempt := 1; attempt <= RetryPerPage; attempt++ { |
||||
resp, err := client.Get(url) |
||||
if err != nil { |
||||
lastErr = err |
||||
log.Printf("[%d/%d] 请求失败 %s -> %v", attempt, RetryPerPage, url, err) |
||||
time.Sleep(time.Duration(1<<uint(attempt)) * time.Second) |
||||
continue |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
resp.Body.Close() |
||||
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode) |
||||
log.Printf("[%d/%d] 请求失败 %s -> %s", attempt, RetryPerPage, url, resp.Status) |
||||
time.Sleep(time.Duration(1<<uint(attempt)) * time.Second) |
||||
continue |
||||
} |
||||
|
||||
body, err := io.ReadAll(resp.Body) |
||||
resp.Body.Close() |
||||
if err != nil { |
||||
lastErr = err |
||||
log.Printf("[%d/%d] 读取响应失败 %s -> %v", attempt, RetryPerPage, url, err) |
||||
time.Sleep(time.Duration(1<<uint(attempt)) * time.Second) |
||||
continue |
||||
} |
||||
|
||||
return string(body), nil |
||||
} |
||||
|
||||
return "", lastErr |
||||
} |
||||
|
||||
// -------------------- HTML 解析 --------------------
|
||||
func extractTitle(htmlContent string) string { |
||||
doc, err := html.Parse(strings.NewReader(htmlContent)) |
||||
if err != nil { |
||||
return "gallery" |
||||
} |
||||
|
||||
var title string |
||||
var findTitle func(*html.Node) |
||||
findTitle = func(n *html.Node) { |
||||
if n.Type == html.ElementNode && n.Data == "title" { |
||||
if n.FirstChild != nil { |
||||
title = n.FirstChild.Data |
||||
return |
||||
} |
||||
} |
||||
for c := n.FirstChild; c != nil; c = c.NextSibling { |
||||
findTitle(c) |
||||
} |
||||
} |
||||
findTitle(doc) |
||||
|
||||
if title == "" { |
||||
return "gallery" |
||||
} |
||||
return title |
||||
} |
||||
|
||||
func extractImageLinks(htmlContent string) []string { |
||||
var links []string |
||||
matches := imgLinkRegex.FindAllStringSubmatch(htmlContent, -1) |
||||
for _, match := range matches { |
||||
if len(match) > 1 { |
||||
links = append(links, match[1]) |
||||
} |
||||
} |
||||
return links |
||||
} |
||||
|
||||
// -------------------- 画廊爬取 --------------------
|
||||
func crawlSingleGallery(client *http.Client, sem *semaphore.Weighted, galleryURL string, stats *CrawlStats) GalleryResult { |
||||
// 获取信号量
|
||||
ctx := context.Background() |
||||
if err := sem.Acquire(ctx, 1); err != nil { |
||||
return GalleryResult{URL: galleryURL, OK: false, Error: err} |
||||
} |
||||
defer sem.Release(1) |
||||
|
||||
// 解析基础URL和key
|
||||
baseURL := strings.TrimRight(galleryURL, "/") |
||||
parsed, err := url.Parse(baseURL) |
||||
if err != nil { |
||||
return GalleryResult{URL: galleryURL, OK: false, Error: err} |
||||
} |
||||
|
||||
pathParts := strings.Split(parsed.Path, "/") |
||||
key := pathParts[len(pathParts)-1] |
||||
jsonName := key + ".json" |
||||
|
||||
var folderPath string |
||||
jsonData := make(map[string]string) |
||||
imgCount := 1 |
||||
lastPage := false |
||||
|
||||
for page := 0; page < MaxPage && !lastPage; page++ { |
||||
pageURL := baseURL |
||||
if page > 0 { |
||||
pageURL = fmt.Sprintf("%s?p=%d", baseURL, page) |
||||
} |
||||
|
||||
htmlContent, err := fetchPage(client, pageURL) |
||||
if err != nil { |
||||
log.Printf("获取页面失败 %s: %v", pageURL, err) |
||||
continue |
||||
} |
||||
|
||||
// 提取标题和创建文件夹
|
||||
title := extractTitle(htmlContent) |
||||
cleanTitle := cleanFolderName(title) |
||||
folderPath = filepath.Join(DownloadsDir, cleanTitle) |
||||
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil { |
||||
return GalleryResult{URL: galleryURL, OK: false, Error: err} |
||||
} |
||||
|
||||
// 检查JSON文件是否已存在
|
||||
jsonPath := filepath.Join(folderPath, jsonName) |
||||
if _, err := os.Stat(jsonPath); err == nil { |
||||
stats.mu.Lock() |
||||
stats.Skipped++ |
||||
stats.mu.Unlock() |
||||
log.Printf("%s 已存在,跳过", jsonName) |
||||
return GalleryResult{URL: galleryURL, OK: true} |
||||
} |
||||
|
||||
log.Printf("当前页码:%d %s", page+1, pageURL) |
||||
|
||||
// 提取图片链接
|
||||
links := extractImageLinks(htmlContent) |
||||
if len(links) == 0 { |
||||
log.Printf("本页无图片入口,视为最后一页") |
||||
lastPage = true |
||||
continue |
||||
} |
||||
|
||||
// 处理图片链接
|
||||
for _, link := range links { |
||||
// 检查是否重复(简单的重复检测)
|
||||
isDuplicate := false |
||||
for _, existingLink := range jsonData { |
||||
if existingLink == link { |
||||
isDuplicate = true |
||||
lastPage = true |
||||
break |
||||
} |
||||
} |
||||
if isDuplicate { |
||||
break |
||||
} |
||||
|
||||
jsonData[fmt.Sprintf("%04d", imgCount)] = link |
||||
imgCount++ |
||||
} |
||||
} |
||||
|
||||
// 保存JSON文件
|
||||
if len(jsonData) > 0 { |
||||
jsonPath := filepath.Join(folderPath, jsonName) |
||||
data, err := json.MarshalIndent(jsonData, "", " ") |
||||
if err != nil { |
||||
return GalleryResult{URL: galleryURL, OK: false, Error: err} |
||||
} |
||||
|
||||
if err := os.WriteFile(jsonPath, data, 0644); err != nil { |
||||
return GalleryResult{URL: galleryURL, OK: false, Error: err} |
||||
} |
||||
|
||||
log.Printf("保存成功 -> %s (%d 张)", jsonPath, len(jsonData)) |
||||
stats.mu.Lock() |
||||
stats.Success++ |
||||
stats.mu.Unlock() |
||||
return GalleryResult{URL: galleryURL, OK: true} |
||||
} else { |
||||
log.Printf("%s 未解析到任何图片链接", key) |
||||
stats.mu.Lock() |
||||
stats.Failed++ |
||||
stats.mu.Unlock() |
||||
return GalleryResult{URL: galleryURL, OK: false, Error: fmt.Errorf("未解析到图片链接")} |
||||
} |
||||
} |
||||
|
||||
// -------------------- 主流程 --------------------
|
||||
func UrlDownloader(ip, port string, output *widget.Entry) { |
||||
log.SetFlags(log.LstdFlags | log.Lshortfile) |
||||
|
||||
// 确保下载目录存在
|
||||
if err := ensureDownloadsDir(); err != nil { |
||||
log.Fatalf("创建下载目录失败: %v", err) |
||||
} |
||||
|
||||
// 加载目标URL
|
||||
targets, err := loadTargets() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
// 加载失败记录
|
||||
failed, err := loadFailedUrl() |
||||
if err != nil { |
||||
log.Printf("加载失败记录失败: %v", err) |
||||
failed = []string{} |
||||
} |
||||
|
||||
// 合并URL列表(去重)
|
||||
allURLs := make([]string, 0) |
||||
seen := make(map[string]bool) |
||||
|
||||
// 优先添加失败记录
|
||||
if len(failed) > 0 { |
||||
log.Printf("优先重试上次失败画廊: %d 个", len(failed)) |
||||
for _, url := range failed { |
||||
if !seen[url] { |
||||
allURLs = append(allURLs, url) |
||||
seen[url] = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 添加新目标
|
||||
for _, url := range targets { |
||||
if !seen[url] { |
||||
allURLs = append(allURLs, url) |
||||
seen[url] = true |
||||
} |
||||
} |
||||
|
||||
if len(allURLs) == 0 { |
||||
log.Println("没有需要处理的URL") |
||||
return |
||||
} |
||||
|
||||
log.Printf("开始处理 %d 个画廊", len(allURLs)) |
||||
|
||||
// 创建HTTP客户端
|
||||
proxy := ip + port |
||||
client := createHTTPClient(proxy) |
||||
|
||||
// 创建信号量控制并发
|
||||
sem := semaphore.NewWeighted(int64(Concurrency)) |
||||
stats := &CrawlStats{Total: len(allURLs)} |
||||
|
||||
// 使用WaitGroup等待所有任务完成
|
||||
var wg sync.WaitGroup |
||||
results := make(chan GalleryResult, len(allURLs)) |
||||
|
||||
// 启动所有爬取任务
|
||||
for _, galleryURL := range allURLs { |
||||
wg.Add(1) |
||||
go func(url string) { |
||||
defer wg.Done() |
||||
result := crawlSingleGallery(client, sem, url, stats) |
||||
results <- result |
||||
}(galleryURL) |
||||
} |
||||
|
||||
// 等待所有任务完成
|
||||
wg.Wait() |
||||
close(results) |
||||
|
||||
// 收集失败结果
|
||||
var newFailed []string |
||||
for result := range results { |
||||
if !result.OK { |
||||
newFailed = append(newFailed, result.URL) |
||||
log.Printf("画廊处理失败 %s: %v", result.URL, result.Error) |
||||
} |
||||
} |
||||
|
||||
// 处理失败记录
|
||||
if len(newFailed) > 0 { |
||||
if err := saveFailedUrl(newFailed); err != nil { |
||||
log.Printf("保存失败记录失败: %v", err) |
||||
} else { |
||||
log.Printf("本轮仍有 %d 个画廊失败,已写入 %s", len(newFailed), FailedRecordUrl) |
||||
} |
||||
} else { |
||||
// 删除失败记录文件
|
||||
if err := os.Remove(FailedRecordUrl); err != nil && !os.IsNotExist(err) { |
||||
log.Printf("删除失败记录文件失败: %v", err) |
||||
} else { |
||||
log.Println("全部画廊抓取完成!") |
||||
} |
||||
} |
||||
|
||||
// 输出统计信息
|
||||
log.Printf("统计信息: 总计=%d, 成功=%d, 失败=%d, 跳过=%d", |
||||
stats.Total, stats.Success, stats.Failed, stats.Skipped) |
||||
} |
||||
@ -0,0 +1,316 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"fyne.io/fyne/v2/widget" |
||||
"github.com/schollz/progressbar/v3" |
||||
) |
||||
|
||||
// -------------------- 全局变量 --------------------
|
||||
var ( |
||||
imgURLRegex = regexp.MustCompile(`<img id="img" src="(.*?)"`) |
||||
extRegex = regexp.MustCompile(`\.(jpg|jpeg|png|gif|webp)$`) |
||||
logger = log.New(os.Stdout, "", log.LstdFlags|log.Lmsgprefix) |
||||
) |
||||
|
||||
// -------------------- 数据结构 --------------------
|
||||
type DownloadTask struct { |
||||
ImgPath string `json:"img_path"` |
||||
ImgURL string `json:"img_url"` |
||||
} |
||||
|
||||
// -------------------- 工具函数 --------------------
|
||||
func loadFailedImg() []DownloadTask { |
||||
if _, err := os.Stat(FailedRecordImg); os.IsNotExist(err) { |
||||
return []DownloadTask{} |
||||
} |
||||
|
||||
data, err := os.ReadFile(FailedRecordImg) |
||||
if err != nil { |
||||
logger.Printf("加载失败记录失败 -> %v", err) |
||||
return []DownloadTask{} |
||||
} |
||||
|
||||
var tasks []DownloadTask |
||||
if err := json.Unmarshal(data, &tasks); err != nil { |
||||
logger.Printf("解析失败记录失败 -> %v", err) |
||||
return []DownloadTask{} |
||||
} |
||||
|
||||
return tasks |
||||
} |
||||
|
||||
func saveFailedImg(failed []DownloadTask) { |
||||
data, err := json.MarshalIndent(failed, "", " ") |
||||
if err != nil { |
||||
logger.Printf("序列化失败记录失败 -> %v", err) |
||||
return |
||||
} |
||||
|
||||
if err := os.WriteFile(FailedRecordImg, data, 0644); err != nil { |
||||
logger.Printf("保存失败记录失败 -> %v", err) |
||||
} |
||||
} |
||||
|
||||
func fileExists(path string) bool { |
||||
_, err := os.Stat(path) |
||||
return err == nil |
||||
} |
||||
|
||||
func getFileExtension(url string) string { |
||||
match := extRegex.FindStringSubmatch(strings.ToLower(url)) |
||||
if len(match) > 1 { |
||||
return match[1] |
||||
} |
||||
return "jpg" // 默认扩展名
|
||||
} |
||||
|
||||
// -------------------- 下载核心 --------------------
|
||||
func downloadOne(client *http.Client, sem chan struct{}, wg *sync.WaitGroup, task DownloadTask, bar *progressbar.ProgressBar) bool { |
||||
defer wg.Done() |
||||
defer func() { <-sem }() |
||||
|
||||
imgPath, imgURL := task.ImgPath, task.ImgURL |
||||
|
||||
for attempt := 1; attempt <= RetryPerImg; attempt++ { |
||||
success := func() bool { |
||||
// 1. 获取详情页
|
||||
resp, err := client.Get(imgURL) |
||||
if err != nil { |
||||
logger.Printf("[ERROR] %s -> %v (尝试 %d/%d)", imgURL, err, attempt, RetryPerImg) |
||||
return false |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
if resp.StatusCode == http.StatusTooManyRequests { |
||||
wait := 1 << (attempt - 1) // 指数退避
|
||||
logger.Printf("[429] 等待 %ds 后重试(%d/%d)", wait, attempt, RetryPerImg) |
||||
time.Sleep(time.Duration(wait) * time.Second) |
||||
return false |
||||
} |
||||
logger.Printf("[HTTP %d] %s", resp.StatusCode, imgURL) |
||||
return false |
||||
} |
||||
|
||||
// 读取响应内容
|
||||
body, err := io.ReadAll(resp.Body) |
||||
if err != nil { |
||||
logger.Printf("[ERROR] 读取响应失败 %s -> %v", imgURL, err) |
||||
return false |
||||
} |
||||
|
||||
// 解析真实图片链接
|
||||
realURLMatch := imgURLRegex.FindStringSubmatch(string(body)) |
||||
if len(realURLMatch) < 2 { |
||||
logger.Printf("未解析到真实图片链接: %s", imgURL) |
||||
return false |
||||
} |
||||
realURL := realURLMatch[1] |
||||
|
||||
// 2. 下载真实图片
|
||||
ext := getFileExtension(realURL) |
||||
finalPath := strings.TrimSuffix(imgPath, filepath.Ext(imgPath)) + "." + ext |
||||
|
||||
// 检查文件是否已存在
|
||||
if fileExists(finalPath) { |
||||
logger.Printf("已存在,跳过: %s", filepath.Base(finalPath)) |
||||
bar.Add(1) |
||||
return true |
||||
} |
||||
|
||||
// 创建目录
|
||||
if err := os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil { |
||||
logger.Printf("[ERROR] 创建目录失败 %s -> %v", filepath.Dir(finalPath), err) |
||||
return false |
||||
} |
||||
|
||||
// 下载图片
|
||||
imgResp, err := client.Get(realURL) |
||||
if err != nil { |
||||
logger.Printf("[ERROR] 下载图片失败 %s -> %v", realURL, err) |
||||
return false |
||||
} |
||||
defer imgResp.Body.Close() // 修复:应该是 Body.Close()
|
||||
|
||||
if imgResp.StatusCode != http.StatusOK { |
||||
logger.Printf("[HTTP %d] %s", imgResp.StatusCode, realURL) |
||||
return false |
||||
} |
||||
|
||||
// 创建文件
|
||||
file, err := os.Create(finalPath) |
||||
if err != nil { |
||||
logger.Printf("[ERROR] 创建文件失败 %s -> %v", finalPath, err) |
||||
return false |
||||
} |
||||
defer file.Close() |
||||
|
||||
// 写入文件
|
||||
_, err = io.Copy(file, imgResp.Body) |
||||
if err != nil { |
||||
logger.Printf("[ERROR] 写入文件失败 %s -> %v", finalPath, err) |
||||
return false |
||||
} |
||||
|
||||
logger.Printf("[OK] %s", filepath.Base(finalPath)) |
||||
bar.Add(1) |
||||
return true |
||||
}() |
||||
|
||||
if success { |
||||
return true |
||||
} |
||||
|
||||
if attempt < RetryPerImg { |
||||
time.Sleep(time.Second) // 重试前等待
|
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
// -------------------- 扫描待下载列表 --------------------
|
||||
func scanTasks() ([]DownloadTask, error) { |
||||
var tasks []DownloadTask |
||||
|
||||
err := filepath.Walk(DownloadDir, func(path string, info os.FileInfo, err error) error { |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if info.IsDir() || filepath.Ext(path) != ".json" { |
||||
return nil |
||||
} |
||||
|
||||
// 读取JSON文件
|
||||
data, err := os.ReadFile(path) |
||||
if err != nil { |
||||
logger.Printf("读取JSON文件失败 %s -> %v", path, err) |
||||
return nil |
||||
} |
||||
|
||||
var urlMap map[string]string |
||||
if err := json.Unmarshal(data, &urlMap); err != nil { |
||||
logger.Printf("解析JSON失败 %s -> %v", path, err) |
||||
return nil |
||||
} |
||||
|
||||
dir := filepath.Dir(path) |
||||
for imgName, imgURL := range urlMap { |
||||
imgPathWithoutExt := filepath.Join(dir, imgName) |
||||
|
||||
// 检查文件是否已存在(任意扩展名)
|
||||
exists := false |
||||
for _, ext := range []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} { |
||||
if fileExists(imgPathWithoutExt + ext) { |
||||
exists = true |
||||
break |
||||
} |
||||
} |
||||
|
||||
if !exists { |
||||
tasks = append(tasks, DownloadTask{ |
||||
ImgPath: imgPathWithoutExt, |
||||
ImgURL: imgURL, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
return tasks, err |
||||
} |
||||
|
||||
// -------------------- 主流程 --------------------
|
||||
func ImgDownloader(ip, port string, output *widget.Entry) { |
||||
logger.SetPrefix("[INFO] ") |
||||
|
||||
// 1. 优先重试上次失败
|
||||
failedTasks := loadFailedImg() |
||||
if len(failedTasks) > 0 { |
||||
logger.Printf("优先重试上次失败任务: %d 张", len(failedTasks)) |
||||
} |
||||
|
||||
// 2. 扫描新任务
|
||||
newTasks, err := scanTasks() |
||||
if err != nil { |
||||
logger.Printf("扫描任务失败: %v", err) |
||||
return |
||||
} |
||||
|
||||
// 合并任务
|
||||
tasks := append(failedTasks, newTasks...) |
||||
if len(tasks) == 0 { |
||||
logger.Println("没有需要下载的图片,收工!") |
||||
return |
||||
} |
||||
|
||||
logger.Printf("开始下载 %d 张图片", len(tasks)) |
||||
|
||||
// 3. 创建HTTP客户端
|
||||
proxy := ip + port |
||||
client := createHTTPClient(proxy) |
||||
|
||||
// 4. 创建进度条
|
||||
bar := progressbar.NewOptions(len(tasks), |
||||
progressbar.OptionSetDescription("Downloading"), |
||||
progressbar.OptionSetWriter(os.Stderr), |
||||
progressbar.OptionShowCount(), |
||||
progressbar.OptionShowIts(), |
||||
progressbar.OptionSetWidth(30), |
||||
progressbar.OptionThrottle(100*time.Millisecond), |
||||
progressbar.OptionOnCompletion(func() { |
||||
fmt.Fprint(os.Stderr, "\n") |
||||
}), |
||||
) |
||||
|
||||
// 5. 并发下载
|
||||
var wg sync.WaitGroup |
||||
sem := make(chan struct{}, Concurrency) |
||||
results := make([]bool, len(tasks)) // 修复:使用slice存储结果
|
||||
|
||||
for i, task := range tasks { |
||||
wg.Add(1) |
||||
sem <- struct{}{} // 获取信号量
|
||||
|
||||
go func(idx int, t DownloadTask) { |
||||
results[idx] = downloadOne(client, sem, &wg, t, bar) |
||||
}(i, task) |
||||
} |
||||
|
||||
wg.Wait() |
||||
|
||||
// 6. 统计结果
|
||||
var failedAgain []DownloadTask |
||||
successCount := 0 |
||||
|
||||
for i, success := range results { |
||||
if !success { |
||||
failedAgain = append(failedAgain, tasks[i]) |
||||
} else { |
||||
successCount++ |
||||
} |
||||
} |
||||
|
||||
// 7. 保存失败记录
|
||||
if len(failedAgain) > 0 { |
||||
saveFailedImg(failedAgain) |
||||
logger.Printf("本轮仍有 %d 张下载失败,已写入 %s", len(failedAgain), FailedRecordImg) |
||||
} else { |
||||
os.Remove(FailedRecordImg) |
||||
logger.Printf("全部下载完成!成功 %d 张", successCount) |
||||
} |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
module eh-crawler |
||||
|
||||
go 1.22 |
||||
|
||||
toolchain go1.22.2 |
||||
|
||||
require ( |
||||
fyne.io/fyne/v2 v2.6.3 |
||||
github.com/schollz/progressbar/v3 v3.18.0 |
||||
golang.org/x/net v0.35.0 |
||||
golang.org/x/sync v0.11.0 |
||||
) |
||||
|
||||
require ( |
||||
fyne.io/systray v1.11.0 // indirect |
||||
github.com/BurntSushi/toml v1.4.0 // indirect |
||||
github.com/davecgh/go-spew v1.1.1 // indirect |
||||
github.com/fredbi/uri v1.1.0 // indirect |
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect |
||||
github.com/fyne-io/gl-js v0.2.0 // indirect |
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect |
||||
github.com/fyne-io/image v0.1.1 // indirect |
||||
github.com/fyne-io/oksvg v0.1.0 // indirect |
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect |
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect |
||||
github.com/go-text/render v0.2.0 // indirect |
||||
github.com/go-text/typesetting v0.2.1 // indirect |
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect |
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect |
||||
github.com/hack-pad/safejs v0.1.0 // indirect |
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect |
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect |
||||
github.com/kr/text v0.1.0 // indirect |
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect |
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect |
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect |
||||
github.com/pmezard/go-difflib v1.0.0 // indirect |
||||
github.com/rivo/uniseg v0.4.7 // indirect |
||||
github.com/rymdport/portal v0.4.1 // indirect |
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect |
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect |
||||
github.com/stretchr/testify v1.10.0 // indirect |
||||
github.com/yuin/goldmark v1.7.8 // indirect |
||||
golang.org/x/image v0.24.0 // indirect |
||||
golang.org/x/sys v0.30.0 // indirect |
||||
golang.org/x/term v0.29.0 // indirect |
||||
golang.org/x/text v0.22.0 // indirect |
||||
gopkg.in/yaml.v3 v3.0.1 // indirect |
||||
) |
||||
@ -0,0 +1,94 @@ |
||||
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA= |
||||
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs= |
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= |
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= |
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= |
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= |
||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= |
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= |
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= |
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= |
||||
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= |
||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= |
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= |
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= |
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= |
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= |
||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= |
||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= |
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= |
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= |
||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw= |
||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= |
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= |
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= |
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= |
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= |
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= |
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= |
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= |
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= |
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= |
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= |
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= |
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= |
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= |
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= |
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= |
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= |
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= |
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= |
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= |
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= |
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= |
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= |
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= |
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= |
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= |
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= |
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= |
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= |
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= |
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= |
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= |
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= |
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= |
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= |
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= |
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= |
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA= |
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= |
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= |
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= |
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= |
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= |
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= |
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= |
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= |
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= |
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= |
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= |
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= |
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= |
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= |
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= |
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= |
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= |
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= |
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= |
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= |
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= |
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= |
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||
@ -0,0 +1,85 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"fyne.io/fyne/v2" |
||||
"fyne.io/fyne/v2/app" |
||||
"fyne.io/fyne/v2/container" |
||||
"fyne.io/fyne/v2/widget" |
||||
) |
||||
|
||||
func main() { |
||||
myApp := app.New() |
||||
myWindow := myApp.NewWindow("下载工具") |
||||
myWindow.Resize(fyne.NewSize(800, 600)) |
||||
|
||||
// 创建输入框
|
||||
ipEntry := widget.NewEntry() |
||||
ipEntry.SetText("127.0.0.1") |
||||
ipEntry.Resize(fyne.NewSize(200, ipEntry.MinSize().Height)) |
||||
portEntry := widget.NewEntry() |
||||
portEntry.SetText("7890") |
||||
ipEntry.Resize(fyne.NewSize(150, ipEntry.MinSize().Height)) |
||||
|
||||
// 创建输出框
|
||||
outputText := widget.NewMultiLineEntry() |
||||
outputText.SetPlaceHolder("输出将显示在这里...") |
||||
outputText.Wrapping = fyne.TextWrapWord |
||||
|
||||
// 创建按钮
|
||||
urlButton := widget.NewButton("下载URL", func() { |
||||
ip := ipEntry.Text |
||||
port := portEntry.Text |
||||
// 在输出框中显示信息
|
||||
outputText.SetText(fmt.Sprintf("开始下载URL - IP: %s, Port: %s\n%s", ip, port, outputText.Text)) |
||||
// 调用UrlDownloader函数
|
||||
UrlDownloader(ip, port, outputText) |
||||
}) |
||||
|
||||
imgButton := widget.NewButton("下载图片", func() { |
||||
ip := ipEntry.Text |
||||
port := portEntry.Text |
||||
// 在输出框中显示信息
|
||||
outputText.SetText(fmt.Sprintf("开始下载图片 - IP: %s, Port: %s\n%s", ip, port, outputText.Text)) |
||||
// 调用ImgDownloader函数
|
||||
ImgDownloader(ip, port, outputText) |
||||
}) |
||||
|
||||
clearButton := widget.NewButton("清除输出", func() { |
||||
outputText.SetText("") |
||||
}) |
||||
|
||||
// 创建滚动容器用于输出框
|
||||
scrollContainer := container.NewScroll(outputText) |
||||
scrollContainer.SetMinSize(fyne.NewSize(400, 400)) |
||||
|
||||
// 布局
|
||||
// 左侧按钮容器
|
||||
leftPanel := container.NewVBox( |
||||
urlButton, |
||||
imgButton, |
||||
clearButton, |
||||
widget.NewLabel(""), // 空标签用于间隔
|
||||
) |
||||
|
||||
// 顶部输入框容器
|
||||
inputPanel := container.NewHBox( |
||||
widget.NewLabel("IP:"), |
||||
ipEntry, |
||||
widget.NewLabel("端口:"), |
||||
portEntry, |
||||
) |
||||
|
||||
// 主布局:顶部是输入框,中间是左右分栏
|
||||
content := container.NewBorder( |
||||
inputPanel, // 顶部
|
||||
nil, // 底部
|
||||
leftPanel, // 左侧
|
||||
nil, // 右侧
|
||||
scrollContainer, // 中间
|
||||
) |
||||
|
||||
myWindow.SetContent(content) |
||||
myWindow.ShowAndRun() |
||||
} |
||||
@ -0,0 +1 @@ |
||||
https://e-hentai.org/g/3550066/47d6393550 |
||||
@ -0,0 +1,46 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"time" |
||||
) |
||||
|
||||
// -------------------- 配置常量 --------------------
|
||||
const ( |
||||
Concurrency = 20 // 并发页数
|
||||
MaxPage = 100 // 单专辑最大翻页
|
||||
RetryPerPage = 5 // 单页重试次数
|
||||
Timeout = 10 // 请求超时(秒)
|
||||
ImgSelector = "#gdt" // 图片入口区域选择器(用于参考,实际用正则)
|
||||
FailedRecordUrl = "failed_keys.json" |
||||
DownloadsDir = "downloads" |
||||
TargetsFile = "targets.txt" |
||||
RetryPerImg = 5 // 单图重试次数
|
||||
FailedRecordImg = "failed_downloads.json" |
||||
DownloadDir = "downloads" |
||||
) |
||||
|
||||
// -------------------- HTTP客户端 --------------------
|
||||
func createHTTPClient(proxy string) *http.Client { |
||||
if proxy == "" { |
||||
return &http.Client{ |
||||
Timeout: time.Duration(Timeout) * time.Second, |
||||
} |
||||
} |
||||
|
||||
proxyURL, err := url.Parse(proxy) |
||||
if err != nil { |
||||
log.Fatalf("解析代理地址失败: %v", err) |
||||
} |
||||
|
||||
transport := &http.Transport{ |
||||
Proxy: http.ProxyURL(proxyURL), |
||||
} |
||||
|
||||
return &http.Client{ |
||||
Transport: transport, |
||||
Timeout: time.Duration(Timeout) * time.Second, |
||||
} |
||||
} |
||||
Loading…
Reference in new issue