diff --git a/go.mod b/go.mod index 7faa423..6e10d27 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,11 @@ go 1.23 require github.com/mmcdole/gofeed v1.3.0 +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect +) + require ( github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect @@ -13,5 +18,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/stretchr/testify v1.8.4 // indirect golang.org/x/net v0.4.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/text v0.14.0 // indirect + gorm.io/gorm v1.25.12 ) diff --git a/go.sum b/go.sum index c5ad212..4c4f837 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= @@ -32,6 +36,10 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/main.go b/main.go index c0185ef..9f1eae0 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,8 @@ func main() { provider := search.NewITunesProvider() - podcast, err := provider.SearchPodcasts("golang", &search.SearchOptions{Country: "us", Language: "en-us"}) + // podcast, err := provider.SearchPodcasts("golang", &search.SearchOptions{Country: "us", Language: "en-us"}) + podcast, err := provider.Charts(&search.ChartsOptions{Country: "us", Language: "en-us", Limit: 200}) if err != nil { panic(err) } diff --git a/model/chart_result.go b/model/chart_result.go new file mode 100644 index 0000000..bbb026e --- /dev/null +++ b/model/chart_result.go @@ -0,0 +1,126 @@ +// Package model 提供播客和剧集的数据模型 +package model + +// ChartResult 表示Apple Podcasts排行榜结果 +type ChartResult struct { + Feed Feed `json:"feed"` +} + +// Feed 表示排行榜的Feed信息 +type Feed struct { + Author Author `json:"author"` + Entry []Entry `json:"entry"` +} + +// Author 表示Feed的作者信息 +type Author struct { + Name NameLabel `json:"name"` + URI URILabel `json:"uri"` +} + +// NameLabel 表示带标签的名称 +type NameLabel struct { + Label string `json:"label"` +} + +// URILabel 表示带标签的URI +type URILabel struct { + Label string `json:"label"` +} + +// Entry 表示排行榜中的一个播客条目 +type Entry struct { + Name NameLabel `json:"im:name"` + Image []Image `json:"im:image"` + Summary NameLabel `json:"summary"` + Price Price `json:"im:price"` + ContentType ContentType `json:"im:contentType"` + Rights NameLabel `json:"rights"` + Title NameLabel `json:"title"` + Link Link `json:"link"` + ID ID `json:"id"` + Artist NameLabel `json:"im:artist"` + Category Category `json:"category"` + ReleaseDate ReleaseDate `json:"im:releaseDate"` +} + +// Image 表示播客的图片信息 +type Image struct { + Label string `json:"label"` + Attributes ImageAttribute `json:"attributes"` +} + +// ImageAttribute 表示图片的属性 +type ImageAttribute struct { + Height string `json:"height"` +} + +// Price 表示播客的价格信息 +type Price struct { + Label string `json:"label"` + Attributes PriceAttributes `json:"attributes"` +} + +// PriceAttributes 表示价格的属性 +type PriceAttributes struct { + Amount string `json:"amount"` + Currency string `json:"currency"` +} + +// ContentType 表示内容类型 +type ContentType struct { + Attributes ContentTypeAttributes `json:"attributes"` +} + +// ContentTypeAttributes 表示内容类型的属性 +type ContentTypeAttributes struct { + Term string `json:"term"` + Label string `json:"label"` +} + +// Link 表示链接信息 +type Link struct { + Attributes LinkAttributes `json:"attributes"` +} + +// LinkAttributes 表示链接的属性 +type LinkAttributes struct { + Rel string `json:"rel"` + Type string `json:"type"` + Href string `json:"href"` +} + +// ID 表示播客的ID信息 +type ID struct { + Label string `json:"label"` + Attributes IDAttributes `json:"attributes"` +} + +// IDAttributes 表示ID的属性 +type IDAttributes struct { + ID string `json:"im:id"` +} + +// Category 表示播客的分类信息 +type Category struct { + Attributes CategoryAttributes `json:"attributes"` +} + +// CategoryAttributes 表示分类的属性 +type CategoryAttributes struct { + ID string `json:"im:id"` + Term string `json:"term"` + Scheme string `json:"scheme"` + Label string `json:"label"` +} + +// ReleaseDate 表示发布日期信息 +type ReleaseDate struct { + Label string `json:"label"` + Attributes ReleaseDateAttributes `json:"attributes"` +} + +// ReleaseDateAttributes 表示发布日期的属性 +type ReleaseDateAttributes struct { + Label string `json:"label"` +} diff --git a/model/feed_entry.go b/model/feed_entry.go new file mode 100644 index 0000000..e2bdc89 --- /dev/null +++ b/model/feed_entry.go @@ -0,0 +1,74 @@ +// Package model 提供播客和剧集的数据模型 +package model + +// FeedEntry 表示 Apple Podcasts RSS Feed 中的一个条目 +type FeedEntry struct { + Name struct { + Label string `json:"label"` + } `json:"im:name"` + Image []struct { + Label string `json:"label"` + Attributes struct { + Height string `json:"height"` + } `json:"attributes"` + } `json:"im:image"` + Summary struct { + Label string `json:"label"` + } `json:"summary"` + Price struct { + Label string `json:"label"` + Attributes struct { + Amount string `json:"amount"` + Currency string `json:"currency"` + } `json:"attributes"` + } `json:"im:price"` + ContentType struct { + Attributes struct { + Term string `json:"term"` + Label string `json:"label"` + } `json:"attributes"` + } `json:"im:contentType"` + Rights struct { + Label string `json:"label"` + } `json:"rights"` + Title struct { + Label string `json:"label"` + } `json:"title"` + Link struct { + Attributes struct { + Rel string `json:"rel"` + Type string `json:"type"` + Href string `json:"href"` + } `json:"attributes"` + } `json:"link"` + ID struct { + Label string `json:"label"` + Attributes struct { + ID string `json:"im:id"` + } `json:"attributes"` + } `json:"id"` + Artist struct { + Label string `json:"label"` + } `json:"im:artist"` + Category struct { + Attributes struct { + ID string `json:"im:id"` + Term string `json:"term"` + Scheme string `json:"scheme"` + Label string `json:"label"` + } `json:"attributes"` + } `json:"category"` + ReleaseDate struct { + Label string `json:"label"` + Attributes struct { + Label string `json:"label"` + } `json:"attributes"` + } `json:"im:releaseDate"` +} + +// FeedResponse 表示 Apple Podcasts RSS Feed 的响应 +type FeedResponse struct { + Feed struct { + Entry []FeedEntry `json:"entry"` + } `json:"feed"` +} diff --git a/model/lookup_result.go b/model/lookup_result.go new file mode 100644 index 0000000..c85ff5f --- /dev/null +++ b/model/lookup_result.go @@ -0,0 +1,21 @@ +// Package model 提供播客和剧集的数据模型 +package model + +import ( + "time" +) + +// SearchResult2 表示iTunes API搜索或查询的结果 +type LookupResult struct { + // 结果数量 + ResultCount int `json:"resultCount"` + + // 搜索是否成功 + Successful bool `json:"successful,omitempty"` + + // 搜索结果项目列表 + Items []ApplePodcast `json:"results"` + + // 处理时间 + ProcessedTime time.Time `json:"processedTime,omitempty"` +} diff --git a/model/search.go b/model/search.go index 2a83615..aaf0a86 100644 --- a/model/search.go +++ b/model/search.go @@ -43,3 +43,18 @@ type SearchResult struct { // 处理时间 ProcessedTime time.Time `json:"processedTime,omitempty"` } + +// SearchResult 表示播客搜索的结果 +type SearchResult2 struct { + // 结果数量 + ResultCount int `json:"resultCount,omitempty"` + + // 搜索是否成功 + Successful bool `json:"successful,omitempty"` + + // 搜索结果项目列表 + Items []ApplePodcast `json:"items,omitempty"` + + // 处理时间 + ProcessedTime time.Time `json:"processedTime,omitempty"` +} diff --git a/search/itunes_search.go b/search/itunes_search.go index f3dd899..e0d5f11 100644 --- a/search/itunes_search.go +++ b/search/itunes_search.go @@ -104,7 +104,7 @@ type ChartsOptions struct { } // SearchPodcasts 搜索播客 -func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*model.SearchResult, error) { +func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*model.SearchResult2, error) { if term == "" { return nil, fmt.Errorf("搜索词不能为空") } @@ -178,76 +178,22 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m 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) - } + // var response struct { + // ResultCount int `json:"resultCount"` + // Results []model.ApplePodcast `json:"results"` + // } // 创建搜索结果 - result := &model.SearchResult{ - ResultCount: response.ResultCount, - Successful: true, - Items: make([]model.Item, 0, len(response.Results)), + result := &model.SearchResult2{ + Items: []model.ApplePodcast{}, 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) + 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 } @@ -256,9 +202,10 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m // 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派 // 由于热门播客列表以feed形式返回,为了与SearchResult兼容,需要解析feed并获取每个项目的底层结果 // 这会导致每个结果都有一个HTTP调用,鉴于热门播客列表更新不频繁,建议客户端缓存结果 -func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, error) { +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) @@ -286,55 +233,29 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, er if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } + // fmt.Printf("body: %s\n", string(body)[:3000]) + // fmt.Printf("body: %s\n", string(body)) - // 解析JSON响应 - var response struct { - Feed struct { - Entry []map[string]interface{} `json:"entry"` - } `json:"feed"` - } - - if err := json.Unmarshal(body, &response); err != nil { + var chartResult model.ChartResult + if err := json.Unmarshal(body, &chartResult); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 创建搜索结果 - result := &model.SearchResult{ - Successful: true, - Items: make([]model.Item, 0), - ProcessedTime: time.Now(), + result := &model.SearchResult2{ + Items: make([]model.ApplePodcast, 0), } // 解析结果 - if response.Feed.Entry != nil { - for _, entry := range response.Feed.Entry { + if chartResult.Feed.Entry != nil { + for _, entry := range chartResult.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) - } - } - } - } - } + id := entry.ID.Attributes.ID 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) @@ -358,73 +279,21 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, er if err != nil { continue } + fmt.Printf("lookupBody: %s\n", string(lookupBody)) - var lookupResponse struct { - ResultCount int `json:"resultCount"` - Results []map[string]interface{} `json:"results"` - } - - if err := json.Unmarshal(lookupBody, &lookupResponse); err != nil { + var tempResult model.LookupResult + if err := json.Unmarshal(lookupBody, &tempResult); 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) + 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) + // result.ResultCount = len(result.Items) return result, nil } @@ -455,7 +324,7 @@ func (p *ITunesProvider) buildChartsURL(options *ChartsOptions) string { if options.Limit > 0 { buf += strconv.Itoa(options.Limit) } else { - buf += "20" + buf += "200" } // 添加流派 @@ -468,9 +337,9 @@ func (p *ITunesProvider) buildChartsURL(options *ChartsOptions) string { // 添加限制内容设置 buf += "/explicit=" if options.Explicit { - buf += "Yes" + buf += "true" } else { - buf += "No" + buf += "false" } // 添加JSON格式 diff --git a/search/itunes_search_test.go b/search/itunes_search_test.go deleted file mode 100644 index 6961204..0000000 --- a/search/itunes_search_test.go +++ /dev/null @@ -1,331 +0,0 @@ -package search - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func TestSearchPodcasts_EmptyTerm(t *testing.T) { - // 创建iTunes搜索提供者 - provider := NewITunesProvider() - - // 使用空搜索词调用SearchPodcasts - result, err := provider.SearchPodcasts("", nil) - - // 验证返回错误 - if err == nil { - t.Error("期望返回错误,但没有") - } - - // 验证错误消息 - expectedErrMsg := "搜索词不能为空" - if err.Error() != expectedErrMsg { - t.Errorf("期望错误消息为 %q,但得到 %q", expectedErrMsg, err.Error()) - } - - // 验证结果为nil - if result != nil { - t.Errorf("期望结果为nil,但得到 %+v", result) - } -} - -func TestSearchPodcasts_SuccessfulSearch(t *testing.T) { - // 创建测试服务器 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 验证请求方法 - if r.Method != http.MethodGet { - t.Errorf("期望请求方法为GET,但得到 %s", r.Method) - } - - // 验证请求路径 - if r.URL.Path != "/search" { - t.Errorf("期望请求路径为/search,但得到 %s", r.URL.Path) - } - - // 验证查询参数 - query := r.URL.Query() - if query.Get("term") != "golang" { - t.Errorf("期望term参数为golang,但得到 %s", query.Get("term")) - } - if query.Get("entity") != "podcast" { - t.Errorf("期望entity参数为podcast,但得到 %s", query.Get("entity")) - } - if query.Get("media") != "podcast" { - t.Errorf("期望media参数为podcast,但得到 %s", query.Get("media")) - } - - // 返回模拟响应 - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - // 创建模拟响应数据 - response := map[string]interface{}{ - "resultCount": 2, - "results": []map[string]interface{}{ - { - "collectionId": 1234567890, - "collectionName": "Go编程语言", - "artistName": "Go开发者", - "feedUrl": "https://example.com/feed1", - "artworkUrl100": "https://example.com/artwork1_100.jpg", - "artworkUrl600": "https://example.com/artwork1_600.jpg", - "genres": []string{"Technology", "Education"}, - }, - { - "collectionId": 9876543210, - "collectionName": "Golang讨论", - "artistName": "Golang爱好者", - "feedUrl": "https://example.com/feed2", - "artworkUrl100": "https://example.com/artwork2_100.jpg", - "artworkUrl600": "https://example.com/artwork2_600.jpg", - "genres": []string{"Technology"}, - }, - }, - } - - // 将响应数据编码为JSON并写入响应 - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - // 创建iTunes搜索提供者,并设置测试服务器URL - provider := &ITunesProvider{ - SearchAPIEndpoint: server.URL, - FeedAPIEndpoint: "https://itunes.apple.com", - GenresMap: initITunesGenres(), - Client: &http.Client{Timeout: 20 * time.Second}, - } - - // 调用SearchPodcasts - result, err := provider.SearchPodcasts("golang", nil) - - // 验证没有错误 - if err != nil { - t.Fatalf("期望没有错误,但得到 %v", err) - } - - // 验证结果 - if result == nil { - t.Fatal("期望结果不为nil,但得到nil") - } - - // 验证结果计数 - if result.ResultCount != 2 { - t.Errorf("期望结果计数为2,但得到 %d", result.ResultCount) - } - - // 验证结果成功标志 - if !result.Successful { - t.Error("期望结果成功标志为true,但得到false") - } - - // 验证结果项目数量 - if len(result.Items) != 2 { - t.Errorf("期望结果项目数量为2,但得到 %d", len(result.Items)) - } - - // 验证第一个结果项目 - item1 := result.Items[0] - if item1.CollectionID != 1234567890 { - t.Errorf("期望第一个项目的CollectionID为1234567890,但得到 %d", item1.CollectionID) - } - if item1.Title != "Go编程语言" { - t.Errorf("期望第一个项目的Title为'Go编程语言',但得到 %s", item1.Title) - } - if item1.Author != "Go开发者" { - t.Errorf("期望第一个项目的Author为'Go开发者',但得到 %s", item1.Author) - } - if item1.FeedURL != "https://example.com/feed1" { - t.Errorf("期望第一个项目的FeedURL为'https://example.com/feed1',但得到 %s", item1.FeedURL) - } - if item1.ArtworkURL != "https://example.com/artwork1_100.jpg" { - t.Errorf("期望第一个项目的ArtworkURL为'https://example.com/artwork1_100.jpg',但得到 %s", item1.ArtworkURL) - } - if item1.ArtworkURL600 != "https://example.com/artwork1_600.jpg" { - t.Errorf("期望第一个项目的ArtworkURL600为'https://example.com/artwork1_600.jpg',但得到 %s", item1.ArtworkURL600) - } - if len(item1.Genres) != 2 || item1.Genres[0] != "Technology" || item1.Genres[1] != "Education" { - t.Errorf("期望第一个项目的Genres为[Technology, Education],但得到 %v", item1.Genres) - } - - // 验证第二个结果项目 - item2 := result.Items[1] - if item2.CollectionID != 9876543210 { - t.Errorf("期望第二个项目的CollectionID为9876543210,但得到 %d", item2.CollectionID) - } - if item2.Title != "Golang讨论" { - t.Errorf("期望第二个项目的Title为'Golang讨论',但得到 %s", item2.Title) - } -} - -func TestSearchPodcasts_WithOptions(t *testing.T) { - // 创建测试服务器 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 验证查询参数 - query := r.URL.Query() - - // 验证基本参数 - if query.Get("term") != "golang" { - t.Errorf("期望term参数为golang,但得到 %s", query.Get("term")) - } - - // 验证选项参数 - if query.Get("country") != "cn" { - t.Errorf("期望country参数为cn,但得到 %s", query.Get("country")) - } - if query.Get("genreId") != "1318" { // Technology的ID - t.Errorf("期望genreId参数为1318,但得到 %s", query.Get("genreId")) - } - if query.Get("attribute") != "titleTerm" { - t.Errorf("期望attribute参数为titleTerm,但得到 %s", query.Get("attribute")) - } - if query.Get("limit") != "10" { - t.Errorf("期望limit参数为10,但得到 %s", query.Get("limit")) - } - if query.Get("language") != "zh-cn" { - t.Errorf("期望language参数为zh-cn,但得到 %s", query.Get("language")) - } - if query.Get("explicit") != "No" { - t.Errorf("期望explicit参数为No,但得到 %s", query.Get("explicit")) - } - - // 返回模拟响应 - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - // 创建模拟响应数据 - response := map[string]interface{}{ - "resultCount": 1, - "results": []map[string]interface{}{ - { - "collectionId": 1234567890, - "collectionName": "Go编程语言", - "artistName": "Go开发者", - "feedUrl": "https://example.com/feed1", - "artworkUrl100": "https://example.com/artwork1_100.jpg", - "artworkUrl600": "https://example.com/artwork1_600.jpg", - "genres": []string{"Technology"}, - }, - }, - } - - // 将响应数据编码为JSON并写入响应 - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - // 创建iTunes搜索提供者,并设置测试服务器URL - provider := &ITunesProvider{ - SearchAPIEndpoint: server.URL, - FeedAPIEndpoint: "https://itunes.apple.com", - GenresMap: initITunesGenres(), - Client: &http.Client{Timeout: 20 * time.Second}, - } - - // 创建搜索选项 - options := &SearchOptions{ - Country: "cn", - Genre: "Technology", - Attribute: "titleTerm", - Limit: 10, - Language: "zh-cn", - Explicit: false, - } - - // 调用SearchPodcasts - result, err := provider.SearchPodcasts("golang", options) - - // 验证没有错误 - if err != nil { - t.Fatalf("期望没有错误,但得到 %v", err) - } - - // 验证结果 - if result == nil { - t.Fatal("期望结果不为nil,但得到nil") - } - - // 验证结果计数 - if result.ResultCount != 1 { - t.Errorf("期望结果计数为1,但得到 %d", result.ResultCount) - } - - // 验证结果项目数量 - if len(result.Items) != 1 { - t.Errorf("期望结果项目数量为1,但得到 %d", len(result.Items)) - } -} - -func TestSearchPodcasts_APIError(t *testing.T) { - // 创建测试服务器,返回错误状态码 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - - // 创建iTunes搜索提供者,并设置测试服务器URL - provider := &ITunesProvider{ - SearchAPIEndpoint: server.URL, - FeedAPIEndpoint: "https://itunes.apple.com", - GenresMap: initITunesGenres(), - Client: &http.Client{Timeout: 20 * time.Second}, - } - - // 调用SearchPodcasts - result, err := provider.SearchPodcasts("golang", nil) - - // 验证返回错误 - if err == nil { - t.Error("期望返回错误,但没有") - } - - // 验证错误消息包含状态码 - expectedErrMsg := "API请求失败,状态码: 500" - if err.Error() != expectedErrMsg { - t.Errorf("期望错误消息包含 %q,但得到 %q", expectedErrMsg, err.Error()) - } - - // 验证结果为nil - if result != nil { - t.Errorf("期望结果为nil,但得到 %+v", result) - } -} - -func TestSearchPodcasts_InvalidJSON(t *testing.T) { - // 创建测试服务器,返回无效的JSON - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"resultCount": 1, "results": [invalid json]`)) - })) - defer server.Close() - - // 创建iTunes搜索提供者,并设置测试服务器URL - provider := &ITunesProvider{ - SearchAPIEndpoint: server.URL, - FeedAPIEndpoint: "https://itunes.apple.com", - GenresMap: initITunesGenres(), - Client: &http.Client{Timeout: 20 * time.Second}, - } - - // 调用SearchPodcasts - result, err := provider.SearchPodcasts("golang", nil) - - // 验证返回错误 - if err == nil { - t.Error("期望返回错误,但没有") - } - - // 验证错误消息包含解析失败 - if err != nil && err.Error() != "解析响应失败: invalid character 'i' looking for beginning of value" { - t.Errorf("期望错误消息包含解析失败,但得到 %q", err.Error()) - } - - // 验证结果为nil - if result != nil { - t.Errorf("期望结果为nil,但得到 %+v", result) - } -}