Skip to content

Commit 13df4e2

Browse files
authored
Merge pull request #189 from xonstone/add-getplaylistitems
Add GetPlaylistItems
2 parents 1543b59 + 40b2aa3 commit 13df4e2

File tree

6 files changed

+3745
-2
lines changed

6 files changed

+3745
-2
lines changed

playlist.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ func (c *Client) GetPlaylist(ctx context.Context, playlistID ID, opts ...Request
170170
// playlist's Spotify ID.
171171
//
172172
// Supported options: Limit, Offset, Market, Fields
173+
//
174+
// Deprecated: the Spotify api is moving towards supporting both tracks and episodes. Use GetPlaylistItems which
175+
// supports these.
173176
func (c *Client) GetPlaylistTracks(
174177
ctx context.Context,
175178
playlistID ID,
@@ -190,6 +193,87 @@ func (c *Client) GetPlaylistTracks(
190193
return &result, err
191194
}
192195

196+
// PlaylistItem contains info about an item in a playlist.
197+
type PlaylistItem struct {
198+
// The date and time the track was added to the playlist.
199+
// You can use the TimestampLayout constant to convert
200+
// this field to a time.Time value.
201+
// Warning: very old playlists may not populate this value.
202+
AddedAt string `json:"added_at"`
203+
// The Spotify user who added the track to the playlist.
204+
// Warning: very old playlists may not populate this value.
205+
AddedBy User `json:"added_by"`
206+
// Whether this track is a local file or not.
207+
IsLocal bool `json:"is_local"`
208+
// Information about the track.
209+
Track PlaylistItemTrack `json:"track"`
210+
}
211+
212+
// PlaylistItemTrack is a union type for both tracks and episodes.
213+
type PlaylistItemTrack struct {
214+
Track *FullTrack
215+
Episode *EpisodePage
216+
}
217+
218+
// UnmarshalJSON customises the unmarshalling based on the type flags set.
219+
func (t *PlaylistItemTrack) UnmarshalJSON(b []byte) error {
220+
is := struct {
221+
Episode bool `json:"episode"`
222+
Track bool `json:"track"`
223+
}{}
224+
225+
err := json.Unmarshal(b, &is)
226+
if err != nil {
227+
return err
228+
}
229+
230+
if is.Episode {
231+
err := json.Unmarshal(b, &t.Episode)
232+
if err != nil {
233+
return err
234+
}
235+
}
236+
237+
if is.Track {
238+
err := json.Unmarshal(b, &t.Track)
239+
if err != nil {
240+
return err
241+
}
242+
}
243+
244+
return nil
245+
}
246+
247+
// PlaylistItemPage contains information about items in a playlist.
248+
type PlaylistItemPage struct {
249+
basePage
250+
Items []PlaylistItem `json:"items"`
251+
}
252+
253+
// GetPlaylistItems gets full details of the items in a playlist, given the
254+
// playlist's Spotify ID.
255+
//
256+
// Supported options: Limit, Offset, Market, Fields
257+
func (c *Client) GetPlaylistItems(ctx context.Context, playlistID ID, opts ...RequestOption) (*PlaylistItemPage, error) {
258+
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks", c.baseURL, playlistID)
259+
260+
// Add default as the first option so it gets override by url.Values#Set
261+
opts = append([]RequestOption{AdditionalTypes(EpisodeAdditionalType, TrackAdditionalType)}, opts...)
262+
263+
if params := processOptions(opts...).urlParams.Encode(); params != "" {
264+
spotifyURL += "?" + params
265+
}
266+
267+
var result PlaylistItemPage
268+
269+
err := c.get(ctx, spotifyURL, &result)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
return &result, err
275+
}
276+
193277
// CreatePlaylistForUser creates a playlist for a Spotify user.
194278
// The playlist will be empty until you add tracks to it.
195279
// The playlistName does not need to be unique - a user can have

playlist_test.go

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,136 @@ func TestGetPlaylistTracks(t *testing.T) {
141141
}
142142
}
143143

144+
func TestGetPlaylistItemsEpisodes(t *testing.T) {
145+
client, server := testClientFile(http.StatusOK, "test_data/playlist_items_episodes.json")
146+
defer server.Close()
147+
148+
tracks, err := client.GetPlaylistItems(context.Background(), "playlistID")
149+
if err != nil {
150+
t.Error(err)
151+
}
152+
if tracks.Total != 4 {
153+
t.Errorf("Got %d tracks, expected 47\n", tracks.Total)
154+
}
155+
if len(tracks.Items) == 0 {
156+
t.Fatal("No tracks returned")
157+
}
158+
expected := "112: Dirty Coms"
159+
actual := tracks.Items[0].Track.Episode.Name
160+
if expected != actual {
161+
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
162+
}
163+
added := tracks.Items[0].AddedAt
164+
tm, err := time.Parse(TimestampLayout, added)
165+
if err != nil {
166+
t.Error(err)
167+
}
168+
if f := tm.Format(DateLayout); f != "2022-05-20" {
169+
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
170+
}
171+
}
172+
173+
func TestGetPlaylistItemsTracks(t *testing.T) {
174+
client, server := testClientFile(http.StatusOK, "test_data/playlist_items_tracks.json")
175+
defer server.Close()
176+
177+
tracks, err := client.GetPlaylistItems(context.Background(), "playlistID")
178+
if err != nil {
179+
t.Error(err)
180+
}
181+
if tracks.Total != 2 {
182+
t.Errorf("Got %d tracks, expected 47\n", tracks.Total)
183+
}
184+
if len(tracks.Items) == 0 {
185+
t.Fatal("No tracks returned")
186+
}
187+
expected := "Typhoons"
188+
actual := tracks.Items[0].Track.Track.Name
189+
if expected != actual {
190+
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
191+
}
192+
added := tracks.Items[0].AddedAt
193+
tm, err := time.Parse(TimestampLayout, added)
194+
if err != nil {
195+
t.Error(err)
196+
}
197+
if f := tm.Format(DateLayout); f != "2022-05-20" {
198+
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
199+
}
200+
}
201+
202+
func TestGetPlaylistItemsTracksAndEpisodes(t *testing.T) {
203+
client, server := testClientFile(http.StatusOK, "test_data/playlist_items_episodes_and_tracks.json")
204+
defer server.Close()
205+
206+
tracks, err := client.GetPlaylistItems(context.Background(), "playlistID")
207+
if err != nil {
208+
t.Error(err)
209+
}
210+
if tracks.Total != 4 {
211+
t.Errorf("Got %d tracks, expected 47\n", tracks.Total)
212+
}
213+
if len(tracks.Items) == 0 {
214+
t.Fatal("No tracks returned")
215+
}
216+
217+
expected := "491- The Missing Middle"
218+
actual := tracks.Items[0].Track.Episode.Name
219+
if expected != actual {
220+
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
221+
}
222+
added := tracks.Items[0].AddedAt
223+
tm, err := time.Parse(TimestampLayout, added)
224+
if err != nil {
225+
t.Error(err)
226+
}
227+
if f := tm.Format(DateLayout); f != "2022-05-20" {
228+
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
229+
}
230+
231+
expected = "Typhoons"
232+
actual = tracks.Items[2].Track.Track.Name
233+
if expected != actual {
234+
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
235+
}
236+
added = tracks.Items[0].AddedAt
237+
tm, err = time.Parse(TimestampLayout, added)
238+
if err != nil {
239+
t.Error(err)
240+
}
241+
if f := tm.Format(DateLayout); f != "2022-05-20" {
242+
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
243+
}
244+
}
245+
246+
func TestGetPlaylistItemsOverride(t *testing.T) {
247+
var types string
248+
client, server := testClientString(http.StatusForbidden, "", func(r *http.Request) {
249+
types = r.URL.Query().Get("additional_types")
250+
})
251+
defer server.Close()
252+
253+
_, _ = client.GetPlaylistItems(context.Background(), "playlistID", AdditionalTypes(EpisodeAdditionalType))
254+
255+
if types != "episode" {
256+
t.Errorf("Expected additional type episode, got %s\n", types)
257+
}
258+
}
259+
260+
func TestGetPlaylistItemsDefault(t *testing.T) {
261+
var types string
262+
client, server := testClientString(http.StatusForbidden, "", func(r *http.Request) {
263+
types = r.URL.Query().Get("additional_types")
264+
})
265+
defer server.Close()
266+
267+
_, _ = client.GetPlaylistItems(context.Background(), "playlistID")
268+
269+
if types != "episode,track" {
270+
t.Errorf("Expected additional type episode, got %s\n", types)
271+
}
272+
}
273+
144274
func TestUserFollowsPlaylist(t *testing.T) {
145275
client, server := testClientString(http.StatusOK, `[ true, false ]`)
146276
defer server.Close()
@@ -160,7 +290,7 @@ var newPlaylist = `
160290
"collaborative": %t,
161291
"description": "Test Description",
162292
"external_urls": {
163-
"spotify": "http://open.spotify.com/user/thelinmichael/playlist/7d2D2S200NyUE5KYs80PwO"
293+
"spotify": "api.http://open.spotify.com/user/thelinmichael/playlist/7d2D2S200NyUE5KYs80PwO"
164294
},
165295
"followers": {
166296
"href": null,
@@ -172,7 +302,7 @@ var newPlaylist = `
172302
"name": "A New Playlist",
173303
"owner": {
174304
"external_urls": {
175-
"spotify": "http://open.spotify.com/user/thelinmichael"
305+
"spotify": "api.http://open.spotify.com/user/thelinmichael"
176306
},
177307
"href": "https://api.spotify.com/v1/users/thelinmichael",
178308
"id": "thelinmichael",

request_options.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package spotify
33
import (
44
"net/url"
55
"strconv"
6+
"strings"
67
)
78

89
type RequestOption func(*requestOptions)
@@ -110,6 +111,28 @@ func Timerange(timerange Range) RequestOption {
110111
}
111112
}
112113

114+
type AdditionalType string
115+
116+
const (
117+
EpisodeAdditionalType = "episode"
118+
TrackAdditionalType = "track"
119+
)
120+
121+
// AdditionalTypes is a list of item types that your client supports besides the default track type.
122+
// Valid types are: EpisodeAdditionalType and TrackAdditionalType.
123+
func AdditionalTypes(types ...AdditionalType) RequestOption {
124+
strTypes := make([]string, len(types))
125+
for i, t := range types {
126+
strTypes[i] = string(t)
127+
}
128+
129+
csv := strings.Join(strTypes, ",")
130+
131+
return func(o *requestOptions) {
132+
o.urlParams.Set("additional_types", csv)
133+
}
134+
}
135+
113136
func processOptions(options ...RequestOption) requestOptions {
114137
o := requestOptions{
115138
urlParams: url.Values{},

0 commit comments

Comments
 (0)