// Package search 提供播客搜索功能 package search import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "time" "gitea.malaihome.work/ans/podcast_search/model" ) // SearchProvider 定义搜索提供者接口 type SearchProvider interface { // SearchPodcasts 搜索播客 SearchPodcasts(term string, options *SearchOptions) (*model.SearchResult, error) // Charts 获取热门播客列表 Charts(options *ChartsOptions) (*model.SearchResult, error) // Genres 获取可用的播客流派列表 Genres() []string } // ITunesProvider 实现iTunes搜索提供者 type ITunesProvider struct { // 搜索API端点 SearchAPIEndpoint string // Feed API端点 FeedAPIEndpoint string // 流派映射 GenresMap map[string]int // HTTP客户端 Client *http.Client } // NewITunesProvider 创建新的iTunes搜索提供者 func NewITunesProvider() *ITunesProvider { return &ITunesProvider{ SearchAPIEndpoint: "https://itunes.apple.com/search", FeedAPIEndpoint: "https://itunes.apple.com", GenresMap: initITunesGenres(), Client: &http.Client{Timeout: 20 * time.Second}, } } // initITunesGenres 初始化iTunes流派映射 func initITunesGenres() map[string]int { return map[string]int{ "": -1, "Arts": 1301, "Business": 1321, "Comedy": 1303, "Education": 1304, "Fiction": 1483, "Government": 1511, "Health & Fitness": 1512, "History": 1487, "Kids & Family": 1305, "Leisure": 1502, "Music": 1301, "News": 1489, "Religion & Spirituality": 1314, "Science": 1533, "Society & Culture": 1324, "Sports": 1545, "TV & Film": 1309, "Technology": 1318, "True Crime": 1488, } } // SearchOptions 搜索选项 type SearchOptions struct { // 国家 Country string // 流派 Genre string // 属性 Attribute string // 限制结果数量 Limit int // 语言 Language string // 是否包含限制内容 Explicit bool } // ChartsOptions 热门播客选项 type ChartsOptions struct { // 国家 Country string // 语言 Language string // 限制结果数量 Limit int // 是否包含限制内容 Explicit bool // 流派 Genre string // 额外的查询参数 QueryParams map[string]string } // SearchPodcasts 搜索播客 func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*model.SearchResult2, error) { if term == "" { return nil, fmt.Errorf("搜索词不能为空") } // 构建查询参数 params := url.Values{} params.Add("term", term) params.Add("entity", "podcast") params.Add("media", "podcast") if options != nil { if options.Country != "" { 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 options.Attribute != "" { params.Add("attribute", options.Attribute) } if options.Limit > 0 { params.Add("limit", strconv.Itoa(options.Limit)) } if options.Language != "" { params.Add("language", options.Language) } if options.Explicit { params.Add("explicit", "Yes") } else { params.Add("explicit", "No") } } // 构建URL url := fmt.Sprintf("%s?%s", p.SearchAPIEndpoint, params.Encode()) fmt.Printf("url: %s\n", url) // 发送请求 req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } // 设置User-Agent req.Header.Set("User-Agent", "PodcastSearch-Go/0.1") // 发送请求 resp, err := p.Client.Do(req) if err != nil { return nil, fmt.Errorf("发送请求失败: %w", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API请求失败,状态码: %d", resp.StatusCode) } // 读取响应内容 body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } fmt.Printf("body: %s\n", string(body)) // 解析JSON响应 // var response struct { // ResultCount int `json:"resultCount"` // Results []model.ApplePodcast `json:"results"` // } // 创建搜索结果 result := &model.SearchResult2{ Items: []model.ApplePodcast{}, ProcessedTime: time.Now(), } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } result.ResultCount = len(result.Items) result.Successful = true return result, nil } // Charts 获取热门播客列表 // 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派 // 由于热门播客列表以feed形式返回,为了与SearchResult兼容,需要解析feed并获取每个项目的底层结果 // 这会导致每个结果都有一个HTTP调用,鉴于热门播客列表更新不频繁,建议客户端缓存结果 func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, error) { // 构建URL url := p.buildChartsURL(options) fmt.Printf("url: %s\n", url) // 发送请求 req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } // 设置User-Agent req.Header.Set("User-Agent", "PodcastSearch-Go/0.1") // 发送请求 resp, err := p.Client.Do(req) if err != nil { return nil, fmt.Errorf("发送请求失败: %w", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API请求失败,状态码: %d", resp.StatusCode) } // 读取响应内容 body, err := io.ReadAll(resp.Body) 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) } // 创建搜索结果 result := &model.SearchResult2{ Items: make([]model.ApplePodcast, 0), } // 解析结果 if chartResult.Feed.Entry != nil { for _, entry := range chartResult.Feed.Entry { // 获取播客ID id := entry.ID.Attributes.ID if id == "" { continue } // 为每个播客获取详细信息 lookupURL := fmt.Sprintf("%s/lookup?id=%s", p.FeedAPIEndpoint, id) lookupReq, err := http.NewRequest("GET", lookupURL, nil) if err != nil { continue } lookupReq.Header.Set("User-Agent", "PodcastSearch-Go/0.1") lookupResp, err := p.Client.Do(lookupReq) if err != nil { continue } if lookupResp.StatusCode != http.StatusOK { lookupResp.Body.Close() continue } lookupBody, err := io.ReadAll(lookupResp.Body) lookupResp.Body.Close() if err != nil { continue } fmt.Printf("lookupBody: %s\n", string(lookupBody)) var tempResult model.LookupResult if err := json.Unmarshal(lookupBody, &tempResult); err != nil { continue } if tempResult.ResultCount > 0 && len(tempResult.Items) > 0 { result.Items = append(result.Items, tempResult.Items[0]) } } result.ResultCount = len(result.Items) } // result.ResultCount = len(result.Items) return result, nil } // Genres 返回可用的播客流派列表 func (p *ITunesProvider) Genres() []string { genres := make([]string, 0, len(p.GenresMap)) for genre := range p.GenresMap { if genre != "" { genres = append(genres, genre) } } return genres } // buildChartsURL 构建热门播客URL func (p *ITunesProvider) buildChartsURL(options *ChartsOptions) string { buf := p.FeedAPIEndpoint // 添加国家代码 if options.Country != "" { buf += "/" + options.Country } else { buf += "/us" } // 添加RSS路径和限制 buf += "/rss/toppodcasts/limit=" if options.Limit > 0 { buf += strconv.Itoa(options.Limit) } else { buf += "200" } // 添加流派 if options.Genre != "" { if genreID, ok := p.GenresMap[options.Genre]; ok && genreID != -1 { buf += "/genre=" + strconv.Itoa(genreID) } } // 添加限制内容设置 buf += "/explicit=" if options.Explicit { buf += "true" } else { buf += "false" } // 添加JSON格式 buf += "/json" return buf }