feat(search): add logging, rate limiting, and genre ID support

Enhance the ITunesProvider by adding logging with detailed caller information, implementing rate limiting to prevent API abuse, and supporting genre IDs for more precise searches. Also, update the User-Agent string and improve error handling and logging throughout the search and charts functionality.
This commit is contained in:
2025-03-30 00:14:24 +08:00
parent bd270a09d0
commit 0e04abf4b8
5 changed files with 78 additions and 24 deletions

View File

@@ -7,10 +7,15 @@ import (
"io"
"net/http"
"net/url"
"path"
"runtime"
"strconv"
"strings"
"time"
"gitea.malaihome.work/ans/podcast-search-go/model"
"github.com/sirupsen/logrus"
"go.uber.org/ratelimit"
)
// SearchProvider 定义搜索提供者接口
@@ -32,16 +37,35 @@ type ITunesProvider struct {
// 流派映射
GenresMap map[string]int
// HTTP客户端
Client *http.Client
Client *http.Client
Limiter ratelimit.Limiter
Logger *logrus.Logger
}
// NewITunesProvider 创建新的iTunes搜索提供者
func NewITunesProvider() *ITunesProvider {
// 初始化日志
// 启用调用者信息记录
Logger := logrus.New()
Logger.SetReportCaller(true)
Logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05.000",
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
s := strings.Split(f.Function, ".")
funcname := s[len(s)-1]
_, filename := path.Split(f.File)
return funcname, fmt.Sprintf("%s:%d", filename, f.Line)
},
})
return &ITunesProvider{
SearchAPIEndpoint: "https://itunes.apple.com/search",
FeedAPIEndpoint: "https://itunes.apple.com",
GenresMap: initITunesGenres(),
Client: &http.Client{Timeout: 20 * time.Second},
Limiter: ratelimit.New(20, ratelimit.Per(time.Minute)), // 20 RPM
Logger: Logger,
}
}
@@ -76,7 +100,7 @@ type SearchOptions struct {
// 国家
Country string
// 流派
Genre string
GenreID string
// 属性
Attribute string
// 限制结果数量
@@ -98,7 +122,7 @@ type ChartsOptions struct {
// 是否包含限制内容
Explicit bool
// 流派
Genre string
GenreID string
// 额外的查询参数
QueryParams map[string]string
}
@@ -120,10 +144,8 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
params.Add("country", options.Country)
}
if options.Genre != "" {
if genreID, ok := p.GenresMap[options.Genre]; ok && genreID != -1 {
params.Add("genreId", strconv.Itoa(genreID))
}
if len(options.GenreID) > 0 {
params.Add("genreId", options.GenreID)
}
if options.Attribute != "" {
@@ -139,15 +161,15 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
}
if options.Explicit {
params.Add("explicit", "Yes")
params.Add("explicit", "true")
} else {
params.Add("explicit", "No")
params.Add("explicit", "false")
}
}
// 构建URL
url := fmt.Sprintf("%s?%s", p.SearchAPIEndpoint, params.Encode())
// fmt.Printf("url: %s\n", url)
p.Logger.Infof("url: %s", url)
// 发送请求
req, err := http.NewRequest("GET", url, nil)
@@ -156,10 +178,12 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
}
// 设置User-Agent
req.Header.Set("User-Agent", "PodcastSearch-Go/0.1")
req.Header.Set("User-Agent", "podcast-search-go/0.1")
// 发送请求
p.Limiter.Take()
resp, err := p.Client.Do(req)
p.Logger.Infof("response code: %d", resp.StatusCode)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %w", err)
}
@@ -175,7 +199,6 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
// fmt.Printf("body: %s\n", string(body))
// 创建搜索结果
result := &model.SearchResult2{
@@ -186,6 +209,7 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
p.Logger.Infof("search result, got %d items", len(result.Items))
result.ResultCount = len(result.Items)
result.Successful = true
@@ -199,7 +223,7 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, error) {
// 构建URL
url := p.buildChartsURL(options)
// fmt.Printf("url: %s\n", url)
p.Logger.Infof("url: %s", url)
// 发送请求
req, err := http.NewRequest("GET", url, nil)
@@ -208,10 +232,13 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
}
// 设置User-Agent
req.Header.Set("User-Agent", "PodcastSearch-Go/0.1")
req.Header.Set("User-Agent", "podcast-search-go/0.1")
// 发送请求
p.Limiter.Take()
resp, err := p.Client.Do(req)
p.Logger.Infof("response code: %d", resp.StatusCode)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %w", err)
}
@@ -227,13 +254,12 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
// fmt.Printf("body: %s\n", string(body)[:3000])
// fmt.Printf("body: %s\n", string(body))
var chartResult model.ChartResult
if err := json.Unmarshal(body, &chartResult); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
p.Logger.Infof("chart result, got %d items", len(chartResult.Feed.Entry))
// 创建搜索结果
result := &model.SearchResult2{
@@ -252,13 +278,16 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
// 为每个播客获取详细信息
lookupURL := fmt.Sprintf("%s/lookup?id=%s", p.FeedAPIEndpoint, id)
p.Logger.Infof("lookup url: %s", lookupURL)
lookupReq, err := http.NewRequest("GET", lookupURL, nil)
if err != nil {
continue
}
lookupReq.Header.Set("User-Agent", "PodcastSearch-Go/0.1")
req.Header.Set("User-Agent", "podcast-search-go/0.1")
p.Limiter.Take()
lookupResp, err := p.Client.Do(lookupReq)
p.Logger.Infof("lookup response code: %d", lookupResp.StatusCode)
if err != nil {
continue
}
@@ -273,12 +302,12 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
if err != nil {
continue
}
// fmt.Printf("lookupBody: %s\n", string(lookupBody))
var lookupResult model.LookupResult
if err := json.Unmarshal(lookupBody, &lookupResult); err != nil {
continue
}
p.Logger.Infof("lookup result, collection id: %d, collection name: %s", lookupResult.Items[0].CollectionID, lookupResult.Items[0].CollectionName)
if lookupResult.ResultCount > 0 && len(lookupResult.Items) > 0 {
lookupResult.Items[0].Summary = entry.Summary.Label
@@ -323,10 +352,8 @@ func (p *ITunesProvider) buildChartsURL(options *ChartsOptions) string {
}
// 添加流派
if options.Genre != "" {
if genreID, ok := p.GenresMap[options.Genre]; ok && genreID != -1 {
buf += "/genre=" + strconv.Itoa(genreID)
}
if len(options.GenreID) > 0 {
buf += "/genre=" + options.GenreID
}
// 添加限制内容设置