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:
@@ -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
|
||||
}
|
||||
|
||||
// 添加限制内容设置
|
||||
|
||||
Reference in New Issue
Block a user