get all fields from apple podcast

This commit is contained in:
2025-03-28 23:52:46 +08:00
parent fdde34e1f0
commit 70b8e330cc
9 changed files with 285 additions and 496 deletions

8
go.mod
View File

@@ -4,6 +4,11 @@ go 1.23
require github.com/mmcdole/gofeed v1.3.0 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 ( require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // 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/modern-go/reflect2 v1.0.2 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/net v0.4.0 // 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
) )

8
go.sum
View File

@@ -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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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= 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.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 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=

View File

@@ -10,7 +10,8 @@ func main() {
provider := search.NewITunesProvider() 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 { if err != nil {
panic(err) panic(err)
} }

126
model/chart_result.go Normal file
View File

@@ -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"`
}

74
model/feed_entry.go Normal file
View File

@@ -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"`
}

21
model/lookup_result.go Normal file
View File

@@ -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"`
}

View File

@@ -43,3 +43,18 @@ type SearchResult struct {
// 处理时间 // 处理时间
ProcessedTime time.Time `json:"processedTime,omitempty"` 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"`
}

View File

@@ -104,7 +104,7 @@ type ChartsOptions struct {
} }
// SearchPodcasts 搜索播客 // 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 == "" { if term == "" {
return nil, fmt.Errorf("搜索词不能为空") return nil, fmt.Errorf("搜索词不能为空")
} }
@@ -178,76 +178,22 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
fmt.Printf("body: %s\n", string(body)) fmt.Printf("body: %s\n", string(body))
// 解析JSON响应 // 解析JSON响应
var response struct { // var response struct {
ResultCount int `json:"resultCount"` // ResultCount int `json:"resultCount"`
Results []map[string]interface{} `json:"results"` // Results []model.ApplePodcast `json:"results"`
} // }
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
// 创建搜索结果 // 创建搜索结果
result := &model.SearchResult{ result := &model.SearchResult2{
ResultCount: response.ResultCount, Items: []model.ApplePodcast{},
Successful: true,
Items: make([]model.Item, 0, len(response.Results)),
ProcessedTime: time.Now(), ProcessedTime: time.Now(),
} }
// 解析结果 if err := json.Unmarshal(body, &result); err != nil {
for _, itemData := range response.Results { return nil, fmt.Errorf("解析响应失败: %w", err)
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)
result.Successful = true
return result, nil return result, nil
} }
@@ -256,9 +202,10 @@ func (p *ITunesProvider) SearchPodcasts(term string, options *SearchOptions) (*m
// 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派 // 可选参数包括国家、语言、结果数量限制、是否包含限制内容和流派
// 由于热门播客列表以feed形式返回为了与SearchResult兼容需要解析feed并获取每个项目的底层结果 // 由于热门播客列表以feed形式返回为了与SearchResult兼容需要解析feed并获取每个项目的底层结果
// 这会导致每个结果都有一个HTTP调用鉴于热门播客列表更新不频繁建议客户端缓存结果 // 这会导致每个结果都有一个HTTP调用鉴于热门播客列表更新不频繁建议客户端缓存结果
func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, error) { func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult2, error) {
// 构建URL // 构建URL
url := p.buildChartsURL(options) url := p.buildChartsURL(options)
fmt.Printf("url: %s\n", url)
// 发送请求 // 发送请求
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
@@ -286,55 +233,29 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, er
if err != nil { if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err) return nil, fmt.Errorf("读取响应失败: %w", err)
} }
// fmt.Printf("body: %s\n", string(body)[:3000])
// fmt.Printf("body: %s\n", string(body))
// 解析JSON响应 var chartResult model.ChartResult
var response struct { if err := json.Unmarshal(body, &chartResult); err != nil {
Feed struct {
Entry []map[string]interface{} `json:"entry"`
} `json:"feed"`
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err) return nil, fmt.Errorf("解析响应失败: %w", err)
} }
// 创建搜索结果 // 创建搜索结果
result := &model.SearchResult{ result := &model.SearchResult2{
Successful: true, Items: make([]model.ApplePodcast, 0),
Items: make([]model.Item, 0),
ProcessedTime: time.Now(),
} }
// 解析结果 // 解析结果
if response.Feed.Entry != nil { if chartResult.Feed.Entry != nil {
for _, entry := range response.Feed.Entry { for _, entry := range chartResult.Feed.Entry {
// 获取播客ID // 获取播客ID
var id string id := entry.ID.Attributes.ID
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 == "" { if id == "" {
continue 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) lookupURL := fmt.Sprintf("%s/lookup?id=%s", p.FeedAPIEndpoint, id)
lookupReq, err := http.NewRequest("GET", lookupURL, nil) lookupReq, err := http.NewRequest("GET", lookupURL, nil)
@@ -358,73 +279,21 @@ func (p *ITunesProvider) Charts(options *ChartsOptions) (*model.SearchResult, er
if err != nil { if err != nil {
continue continue
} }
fmt.Printf("lookupBody: %s\n", string(lookupBody))
var lookupResponse struct { var tempResult model.LookupResult
ResultCount int `json:"resultCount"` if err := json.Unmarshal(lookupBody, &tempResult); err != nil {
Results []map[string]interface{} `json:"results"`
}
if err := json.Unmarshal(lookupBody, &lookupResponse); err != nil {
continue continue
} }
if lookupResponse.ResultCount > 0 && len(lookupResponse.Results) > 0 { if tempResult.ResultCount > 0 && len(tempResult.Items) > 0 {
itemData := lookupResponse.Results[0] result.Items = append(result.Items, tempResult.Items[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)
} }
result.ResultCount = len(result.Items) // result.ResultCount = len(result.Items)
return result, nil return result, nil
} }
@@ -455,7 +324,7 @@ func (p *ITunesProvider) buildChartsURL(options *ChartsOptions) string {
if options.Limit > 0 { if options.Limit > 0 {
buf += strconv.Itoa(options.Limit) buf += strconv.Itoa(options.Limit)
} else { } else {
buf += "20" buf += "200"
} }
// 添加流派 // 添加流派
@@ -468,9 +337,9 @@ func (p *ITunesProvider) buildChartsURL(options *ChartsOptions) string {
// 添加限制内容设置 // 添加限制内容设置
buf += "/explicit=" buf += "/explicit="
if options.Explicit { if options.Explicit {
buf += "Yes" buf += "true"
} else { } else {
buf += "No" buf += "false"
} }
// 添加JSON格式 // 添加JSON格式

View File

@@ -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)
}
}