// 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 }