generated by Claude-3.7-Sonnet

This commit is contained in:
2025-03-28 22:27:08 +08:00
commit fdde34e1f0
13 changed files with 1581 additions and 0 deletions

480
search/itunes_search.go Normal file
View File

@@ -0,0 +1,480 @@
// 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
}