- 新增 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>
126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
// Package model 提供播客和剧集的数据模型
|
||
package model
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"time"
|
||
|
||
"github.com/mmcdole/gofeed"
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
// Podcast 表示一个播客及其剧集
|
||
type Podcast struct {
|
||
// 播客的唯一标识符
|
||
GUID string `json:"guid,omitempty"`
|
||
|
||
// 播客的URL
|
||
URL string `json:"url,omitempty"`
|
||
|
||
// 播客的链接
|
||
Link string `json:"link,omitempty"`
|
||
|
||
// 播客的标题
|
||
Title string `json:"title,omitempty"`
|
||
|
||
// 播客的描述
|
||
Description string `json:"description,omitempty"`
|
||
|
||
// 播客的图片URL
|
||
Image string `json:"image,omitempty"`
|
||
|
||
// 版权信息
|
||
Copyright string `json:"copyright,omitempty"`
|
||
|
||
// 语言
|
||
Language string `json:"language,omitempty"`
|
||
|
||
// 是否锁定,表示此订阅是否可以被导入到其他平台
|
||
Locked *Locked `json:"locked,omitempty"`
|
||
|
||
// 资金支持信息
|
||
Funding []Funding `json:"funding,omitempty"`
|
||
|
||
// 人员信息
|
||
Persons []Person `json:"persons,omitempty"`
|
||
|
||
// 价值交换信息
|
||
Value []Value `json:"value,omitempty"`
|
||
|
||
// 阻止标签
|
||
Block []Block `json:"block,omitempty"`
|
||
|
||
// 剧集列表
|
||
Episodes []*gofeed.Item `json:"episodes,omitempty"`
|
||
|
||
// 远程项目
|
||
RemoteItems []RemoteItem `json:"remote_items,omitempty"`
|
||
|
||
// 媒体类型
|
||
Medium Medium `json:"medium,omitempty"`
|
||
}
|
||
|
||
// LoadFeed 从URL加载播客RSS订阅,timeout为超时时间,如果timeout<=0则使用默认值30秒。
|
||
// 需要透传取消时请用 LoadFeedContext。
|
||
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 {
|
||
timeout = 30 * time.Second
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||
defer cancel()
|
||
|
||
fp := gofeed.NewParser()
|
||
fp.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||
|
||
fp.Client = &http.Client{}
|
||
// setup proxy
|
||
if proxy != "" {
|
||
proxyURL, err := url.Parse(proxy)
|
||
if err != nil {
|
||
logrus.WithError(err).Errorf("Failed to parse proxy url: %s", proxy)
|
||
return nil, err
|
||
}
|
||
fp.Client = &http.Client{
|
||
Transport: &http.Transport{
|
||
Proxy: http.ProxyURL(proxyURL),
|
||
},
|
||
}
|
||
logrus.Infof("Using proxy: %s", proxy)
|
||
}
|
||
|
||
feed, err := fp.ParseURLWithContext(feedURL, ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析播客订阅失败: %w", err)
|
||
}
|
||
|
||
imageURL := ""
|
||
if feed.Image != nil {
|
||
imageURL = feed.Image.URL
|
||
}
|
||
podcast := &Podcast{
|
||
Title: feed.Title,
|
||
Description: feed.Description,
|
||
Link: feed.Link,
|
||
Image: imageURL,
|
||
Copyright: feed.Copyright,
|
||
Language: feed.Language,
|
||
Episodes: feed.Items,
|
||
Funding: make([]Funding, 0),
|
||
Persons: make([]Person, 0),
|
||
Value: make([]Value, 0),
|
||
Block: make([]Block, 0),
|
||
Medium: MediumPodcast,
|
||
}
|
||
|
||
return podcast, nil
|
||
}
|