// 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.SearchResult, 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 []map[string]interface{} `json:"results"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 创建搜索结果 result := &model.SearchResult{ ResultCount: response.ResultCount, Successful: true, Items: make([]model.Item, 0, len(response.Results)), ProcessedTime: time.Now(), } // 解析结果 for _, itemData := range response.Results { item := model.Item{} // 提取基本信息 if v, ok := itemData["collectionId"]; ok { if id, ok := v.(float64); ok { item.CollectionID = int(id) } } if v, ok := itemData["collectionName"]; ok { if name, ok := v.(string); ok { item.Title = name } } if v, ok := itemData["artistName"]; ok { if name, ok := v.(string); ok { item.Author = name } } if v, ok := itemData["feedUrl"]; ok { if url, ok := v.(string); ok { item.FeedURL = url } } if v, ok := itemData["artworkUrl100"]; ok { if url, ok := v.(string); ok { item.ArtworkURL = url } } if v, ok := itemData["artworkUrl600"]; ok { if url, ok := v.(string); ok { item.ArtworkURL600 = url } } if v, ok := itemData["genres"]; ok { if genres, ok := v.([]interface{}); ok { for _, g := range genres { if genre, ok := g.(string); ok { item.Genres = append(item.Genres, genre) } } } } result.Items = append(result.Items, item) } return result, nil } // Charts 获取热门播客列表 // 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派 // 由于热门播客列表以feed形式返回,为了与SearchResult兼容,需要解析feed并获取每个项目的底层结果 // 这会导致每个结果都有一个HTTP调用,鉴于热门播客列表更新不频繁,建议客户端缓存结果 func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, error) { // 构建URL url := p.buildChartsURL(options) // 发送请求 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) } // 解析JSON响应 var response struct { Feed struct { Entry []map[string]interface{} `json:"entry"` } `json:"feed"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 创建搜索结果 result := &model.SearchResult{ Successful: true, Items: make([]model.Item, 0), ProcessedTime: time.Now(), } // 解析结果 if response.Feed.Entry != nil { for _, entry := range response.Feed.Entry { // 获取播客ID var id string if idObj, ok := entry["id"]; ok { if idMap, ok := idObj.(map[string]interface{}); ok { if attrs, ok := idMap["attributes"]; ok { if attrsMap, ok := attrs.(map[string]interface{}); ok { if imID, ok := attrsMap["im:id"]; ok { id = fmt.Sprintf("%v", imID) } } } } } if id == "" { continue } // 获取播客标题 // 不再使用title变量,直接在需要时获取 if titleObj, ok := entry["title"]; ok { // 移除未使用的titleMap变量 if _, ok := titleObj.(map[string]interface{}); ok { // 处理标题信息,如果需要使用 } } // 为每个播客获取详细信息 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 } var lookupResponse struct { ResultCount int `json:"resultCount"` Results []map[string]interface{} `json:"results"` } if err := json.Unmarshal(lookupBody, &lookupResponse); err != nil { continue } if lookupResponse.ResultCount > 0 && len(lookupResponse.Results) > 0 { itemData := lookupResponse.Results[0] item := model.Item{} // 提取基本信息 if v, ok := itemData["collectionId"]; ok { if id, ok := v.(float64); ok { item.CollectionID = int(id) } } if v, ok := itemData["collectionName"]; ok { if name, ok := v.(string); ok { item.Title = name } } if v, ok := itemData["artistName"]; ok { if name, ok := v.(string); ok { item.Author = name } } if v, ok := itemData["feedUrl"]; ok { if url, ok := v.(string); ok { item.FeedURL = url } } if v, ok := itemData["artworkUrl100"]; ok { if url, ok := v.(string); ok { item.ArtworkURL = url } } if v, ok := itemData["artworkUrl600"]; ok { if url, ok := v.(string); ok { item.ArtworkURL600 = url } } if v, ok := itemData["genres"]; ok { if genres, ok := v.([]interface{}); ok { for _, g := range genres { if genre, ok := g.(string); ok { item.Genres = append(item.Genres, genre) } } } } result.Items = append(result.Items, item) } } } 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 += "20" } // 添加流派 if options.Genre != "" { if genreID, ok := p.GenresMap[options.Genre]; ok && genreID != -1 { buf += "/genre=" + strconv.Itoa(genreID) } } // 添加限制内容设置 buf += "/explicit=" if options.Explicit { buf += "Yes" } else { buf += "No" } // 添加JSON格式 buf += "/json" return buf }