feat: Charts 与 LoadFeed 支持 context 透传取消
- 新增 ChartsContext / LoadFeedContext(原方法转发 context.Background(),兼容不变) - Charts 榜单请求与逐项 lookup 全部改 NewRequestWithContext,循环顶检查 ctx.Err() 提前中止剩余 lookup - 修复 resp 在 err 检查前解引用(网络错误/ctx 取消时必 panic) - 修复 lookup 循环 User-Agent 误设在外层请求上 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,13 +63,19 @@ type Podcast struct {
|
|||||||
Medium Medium `json:"medium,omitempty"`
|
Medium Medium `json:"medium,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFeed 从URL加载播客RSS订阅,timeout为超时时间,如果timeout<=0则使用默认值30秒
|
// LoadFeed 从URL加载播客RSS订阅,timeout为超时时间,如果timeout<=0则使用默认值30秒。
|
||||||
|
// 需要透传取消时请用 LoadFeedContext。
|
||||||
func LoadFeed(feedURL, proxy string, timeout time.Duration) (*Podcast, error) {
|
func LoadFeed(feedURL, proxy string, timeout time.Duration) (*Podcast, error) {
|
||||||
|
return LoadFeedContext(context.Background(), feedURL, proxy, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFeedContext 同 LoadFeed,但请求生命周期挂在调用方 ctx 上:ctx 取消/超时即中断下载。
|
||||||
|
func LoadFeedContext(ctx context.Context, feedURL, proxy string, timeout time.Duration) (*Podcast, error) {
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 30 * time.Second
|
timeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
fp := gofeed.NewParser()
|
fp := gofeed.NewParser()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package search
|
package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -216,17 +217,23 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charts 获取热门播客列表
|
// Charts 获取热门播客列表(无取消语义,等价于 ChartsContext(context.Background(), options))。
|
||||||
|
func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, error) {
|
||||||
|
return p.ChartsContext(context.Background(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChartsContext 获取热门播客列表,榜单抓取与逐项 lookup 的全部请求都挂在调用方 ctx 上:
|
||||||
|
// ctx 取消即中断当前请求并停止剩余 lookup。
|
||||||
// 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派
|
// 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派
|
||||||
// 由于热门播客列表以feed形式返回,为了与SearchResult兼容,需要解析feed并获取每个项目的底层结果
|
// 由于热门播客列表以feed形式返回,为了与SearchResult兼容,需要解析feed并获取每个项目的底层结果
|
||||||
// 这会导致每个结果都有一个HTTP调用,鉴于热门播客列表更新不频繁,建议客户端缓存结果
|
// 这会导致每个结果都有一个HTTP调用,鉴于热门播客列表更新不频繁,建议客户端缓存结果
|
||||||
func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, error) {
|
func (p *ITunesProvider) ChartsContext(ctx context.Context, options *ChartsOptions) (*model.SearchResult2, error) {
|
||||||
// 构建URL
|
// 构建URL
|
||||||
url := p.buildChartsURL(options)
|
url := p.buildChartsURL(options)
|
||||||
p.Logger.Infof("url: %s", url)
|
p.Logger.Infof("url: %s", url)
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -237,12 +244,11 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
|
|||||||
// 发送请求
|
// 发送请求
|
||||||
p.Limiter.Take()
|
p.Limiter.Take()
|
||||||
resp, err := p.Client.Do(req)
|
resp, err := p.Client.Do(req)
|
||||||
p.Logger.Infof("response code: %d", resp.StatusCode)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("发送请求失败: %w", err)
|
return nil, fmt.Errorf("发送请求失败: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
p.Logger.Infof("response code: %d", resp.StatusCode)
|
||||||
|
|
||||||
// 检查响应状态
|
// 检查响应状态
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
@@ -270,6 +276,11 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
|
|||||||
if chartResult.Feed.Entry != nil {
|
if chartResult.Feed.Entry != nil {
|
||||||
cnt := 1
|
cnt := 1
|
||||||
for _, entry := range chartResult.Feed.Entry {
|
for _, entry := range chartResult.Feed.Entry {
|
||||||
|
// 调用方取消:停止剩余 lookup,立即返回。
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// 获取播客ID
|
// 获取播客ID
|
||||||
id := entry.ID.Attributes.ID
|
id := entry.ID.Attributes.ID
|
||||||
|
|
||||||
@@ -281,18 +292,18 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, e
|
|||||||
lookupURL := fmt.Sprintf("%s/lookup?id=%s", p.FeedAPIEndpoint, id)
|
lookupURL := fmt.Sprintf("%s/lookup?id=%s", p.FeedAPIEndpoint, id)
|
||||||
p.Logger.Infof("%03d lookup url: %s", cnt, lookupURL)
|
p.Logger.Infof("%03d lookup url: %s", cnt, lookupURL)
|
||||||
cnt++
|
cnt++
|
||||||
lookupReq, err := http.NewRequest("GET", lookupURL, nil)
|
lookupReq, err := http.NewRequestWithContext(ctx, "GET", lookupURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "podcast-search-go/0.1")
|
lookupReq.Header.Set("User-Agent", "podcast-search-go/0.1")
|
||||||
p.Limiter.Take()
|
p.Limiter.Take()
|
||||||
lookupResp, err := p.Client.Do(lookupReq)
|
lookupResp, err := p.Client.Do(lookupReq)
|
||||||
p.Logger.Infof("lookup response code: %d", lookupResp.StatusCode)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
p.Logger.Infof("lookup response code: %d", lookupResp.StatusCode)
|
||||||
|
|
||||||
if lookupResp.StatusCode != http.StatusOK {
|
if lookupResp.StatusCode != http.StatusOK {
|
||||||
lookupResp.Body.Close()
|
lookupResp.Body.Close()
|
||||||
|
|||||||
Reference in New Issue
Block a user