random-api-go/service/lankong_fetcher.go

225 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"random-api-go/model"
"time"
)
// LankongFetcher 兰空图床获取器
type LankongFetcher struct {
client *http.Client
retryConfig *RetryConfig
}
// RetryConfig 重试配置
type RetryConfig struct {
MaxRetries int // 最大重试次数
BaseDelay time.Duration // 基础延迟
}
// NewLankongFetcher 创建兰空图床获取器
func NewLankongFetcher() *LankongFetcher {
return &LankongFetcher{
client: &http.Client{
Timeout: 60 * time.Second, // 增加超时时间
},
retryConfig: &RetryConfig{
MaxRetries: 7, // 最多重试7次 (0秒/15秒/15秒/30秒/30秒/60秒/60秒/180秒)
BaseDelay: 1 * time.Second, // 基础延迟(实际不使用,使用固定延迟序列)
},
}
}
// NewLankongFetcherWithConfig 创建带自定义配置的兰空图床获取器
func NewLankongFetcherWithConfig(maxRetries int) *LankongFetcher {
return &LankongFetcher{
client: &http.Client{
Timeout: 60 * time.Second,
},
retryConfig: &RetryConfig{
MaxRetries: maxRetries,
BaseDelay: 1 * time.Second,
},
}
}
// LankongResponse 兰空图床API响应
type LankongResponse struct {
Status bool `json:"status"`
Message string `json:"message"`
Data struct {
CurrentPage int `json:"current_page"`
LastPage int `json:"last_page"`
Data []struct {
Links struct {
URL string `json:"url"`
} `json:"links"`
} `json:"data"`
} `json:"data"`
}
// FetchURLs 从兰空图床获取URL列表
func (lf *LankongFetcher) FetchURLs(config *model.LankongConfig) ([]string, error) {
var allURLs []string
baseURL := config.BaseURL
if baseURL == "" {
baseURL = "https://img.czl.net/api/v1/images"
}
for _, albumID := range config.AlbumIDs {
log.Printf("开始获取相册 %s 的图片", albumID)
// 获取第一页以确定总页数
firstPageURL := fmt.Sprintf("%s?album_id=%s&page=1", baseURL, albumID)
response, err := lf.fetchPageWithRetry(firstPageURL, config.APIToken)
if err != nil {
log.Printf("Failed to fetch first page for album %s: %v", albumID, err)
continue
}
totalPages := response.Data.LastPage
log.Printf("相册 %s 共有 %d 页", albumID, totalPages)
// 处理所有页面
albumURLs := []string{}
for page := 1; page <= totalPages; page++ {
reqURL := fmt.Sprintf("%s?album_id=%s&page=%d", baseURL, albumID, page)
pageResponse, err := lf.fetchPageWithRetry(reqURL, config.APIToken)
if err != nil {
log.Printf("Failed to fetch page %d for album %s: %v", page, albumID, err)
continue
}
for _, item := range pageResponse.Data.Data {
if item.Links.URL != "" {
albumURLs = append(albumURLs, item.Links.URL)
}
}
// 进度日志
if page%10 == 0 || page == totalPages {
log.Printf("相册 %s: 已处理 %d/%d 页,收集到 %d 个URL", albumID, page, totalPages, len(albumURLs))
}
}
allURLs = append(allURLs, albumURLs...)
log.Printf("完成相册 %s: 收集到 %d 个URL", albumID, len(albumURLs))
}
return allURLs, nil
}
// fetchPageWithRetry 带重试的页面获取
func (lf *LankongFetcher) fetchPageWithRetry(url string, apiToken string) (*LankongResponse, error) {
var lastErr error
for attempt := 0; attempt <= lf.retryConfig.MaxRetries; attempt++ {
response, err := lf.fetchPage(url, apiToken)
if err == nil {
return response, nil
}
lastErr = err
// 如果是最后一次尝试,不再重试
if attempt == lf.retryConfig.MaxRetries {
break
}
// 计算延迟时间
var delay time.Duration
if isRateLimitError(err) {
// 对于429错误使用固定的延迟序列
delay = getRateLimitDelay(attempt)
log.Printf("遇到频率限制 (尝试 %d/%d): %v等待 %v 后重试", attempt+1, lf.retryConfig.MaxRetries+1, err, delay)
} else {
// 其他错误使用较短的延迟
delay = time.Duration(attempt+1) * time.Second
log.Printf("请求失败 (尝试 %d/%d): %v%v 后重试", attempt+1, lf.retryConfig.MaxRetries+1, err, delay)
}
time.Sleep(delay)
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %v", lf.retryConfig.MaxRetries, lastErr)
}
// getRateLimitDelay 获取频率限制的延迟时间
// 延迟序列0秒 / 15秒 / 15秒 / 30秒 / 30秒 / 60秒 / 60秒 / 180秒
func getRateLimitDelay(attempt int) time.Duration {
delaySequence := []time.Duration{
0 * time.Second, // 第1次重试立即
15 * time.Second, // 第2次重试15秒后
15 * time.Second, // 第3次重试15秒后
30 * time.Second, // 第4次重试30秒后
30 * time.Second, // 第5次重试30秒后
60 * time.Second, // 第6次重试60秒后
60 * time.Second, // 第7次重试60秒后
180 * time.Second, // 第8次重试180秒后
}
if attempt < len(delaySequence) {
return delaySequence[attempt]
}
// 如果超出序列长度,使用最后一个值
return delaySequence[len(delaySequence)-1]
}
// isRateLimitError 检查是否是频率限制错误
func isRateLimitError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
return fmt.Sprintf("%v", err) == "rate limit exceeded (429), need to slow down requests" ||
fmt.Sprintf("%v", errStr) == "API returned status code: 429"
}
// fetchPage 获取兰空图床单页数据
func (lf *LankongFetcher) fetchPage(url string, apiToken string) (*LankongResponse, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Accept", "application/json")
resp, err := lf.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 特殊处理429错误
if resp.StatusCode == 429 {
return nil, fmt.Errorf("rate limit exceeded (429), need to slow down requests")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var lankongResp LankongResponse
if err := json.Unmarshal(body, &lankongResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if !lankongResp.Status {
return nil, fmt.Errorf("API error: %s", lankongResp.Message)
}
return &lankongResp, nil
}