Skip to content

Commit

Permalink
Merge pull request #189 from xonstone/add-getplaylistitems
Browse files Browse the repository at this point in the history
Add GetPlaylistItems
  • Loading branch information
strideynet authored May 20, 2022
2 parents 1543b59 + 40b2aa3 commit 13df4e2
Show file tree
Hide file tree
Showing 6 changed files with 3,745 additions and 2 deletions.
84 changes: 84 additions & 0 deletions playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ func (c *Client) GetPlaylist(ctx context.Context, playlistID ID, opts ...Request
// playlist's Spotify ID.
//
// Supported options: Limit, Offset, Market, Fields
//
// Deprecated: the Spotify api is moving towards supporting both tracks and episodes. Use GetPlaylistItems which
// supports these.
func (c *Client) GetPlaylistTracks(
ctx context.Context,
playlistID ID,
Expand All @@ -190,6 +193,87 @@ func (c *Client) GetPlaylistTracks(
return &result, err
}

// PlaylistItem contains info about an item in a playlist.
type PlaylistItem struct {
// The date and time the track was added to the playlist.
// You can use the TimestampLayout constant to convert
// this field to a time.Time value.
// Warning: very old playlists may not populate this value.
AddedAt string `json:"added_at"`
// The Spotify user who added the track to the playlist.
// Warning: very old playlists may not populate this value.
AddedBy User `json:"added_by"`
// Whether this track is a local file or not.
IsLocal bool `json:"is_local"`
// Information about the track.
Track PlaylistItemTrack `json:"track"`
}

// PlaylistItemTrack is a union type for both tracks and episodes.
type PlaylistItemTrack struct {
Track *FullTrack
Episode *EpisodePage
}

// UnmarshalJSON customises the unmarshalling based on the type flags set.
func (t *PlaylistItemTrack) UnmarshalJSON(b []byte) error {
is := struct {
Episode bool `json:"episode"`
Track bool `json:"track"`
}{}

err := json.Unmarshal(b, &is)
if err != nil {
return err
}

if is.Episode {
err := json.Unmarshal(b, &t.Episode)
if err != nil {
return err
}
}

if is.Track {
err := json.Unmarshal(b, &t.Track)
if err != nil {
return err
}
}

return nil
}

// PlaylistItemPage contains information about items in a playlist.
type PlaylistItemPage struct {
basePage
Items []PlaylistItem `json:"items"`
}

// GetPlaylistItems gets full details of the items in a playlist, given the
// playlist's Spotify ID.
//
// Supported options: Limit, Offset, Market, Fields
func (c *Client) GetPlaylistItems(ctx context.Context, playlistID ID, opts ...RequestOption) (*PlaylistItemPage, error) {
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks", c.baseURL, playlistID)

// Add default as the first option so it gets override by url.Values#Set
opts = append([]RequestOption{AdditionalTypes(EpisodeAdditionalType, TrackAdditionalType)}, opts...)

if params := processOptions(opts...).urlParams.Encode(); params != "" {
spotifyURL += "?" + params
}

var result PlaylistItemPage

err := c.get(ctx, spotifyURL, &result)
if err != nil {
return nil, err
}

return &result, err
}

// CreatePlaylistForUser creates a playlist for a Spotify user.
// The playlist will be empty until you add tracks to it.
// The playlistName does not need to be unique - a user can have
Expand Down
134 changes: 132 additions & 2 deletions playlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,136 @@ func TestGetPlaylistTracks(t *testing.T) {
}
}

func TestGetPlaylistItemsEpisodes(t *testing.T) {
client, server := testClientFile(http.StatusOK, "test_data/playlist_items_episodes.json")
defer server.Close()

tracks, err := client.GetPlaylistItems(context.Background(), "playlistID")
if err != nil {
t.Error(err)
}
if tracks.Total != 4 {
t.Errorf("Got %d tracks, expected 47\n", tracks.Total)
}
if len(tracks.Items) == 0 {
t.Fatal("No tracks returned")
}
expected := "112: Dirty Coms"
actual := tracks.Items[0].Track.Episode.Name
if expected != actual {
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
}
added := tracks.Items[0].AddedAt
tm, err := time.Parse(TimestampLayout, added)
if err != nil {
t.Error(err)
}
if f := tm.Format(DateLayout); f != "2022-05-20" {
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
}
}

func TestGetPlaylistItemsTracks(t *testing.T) {
client, server := testClientFile(http.StatusOK, "test_data/playlist_items_tracks.json")
defer server.Close()

tracks, err := client.GetPlaylistItems(context.Background(), "playlistID")
if err != nil {
t.Error(err)
}
if tracks.Total != 2 {
t.Errorf("Got %d tracks, expected 47\n", tracks.Total)
}
if len(tracks.Items) == 0 {
t.Fatal("No tracks returned")
}
expected := "Typhoons"
actual := tracks.Items[0].Track.Track.Name
if expected != actual {
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
}
added := tracks.Items[0].AddedAt
tm, err := time.Parse(TimestampLayout, added)
if err != nil {
t.Error(err)
}
if f := tm.Format(DateLayout); f != "2022-05-20" {
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
}
}

func TestGetPlaylistItemsTracksAndEpisodes(t *testing.T) {
client, server := testClientFile(http.StatusOK, "test_data/playlist_items_episodes_and_tracks.json")
defer server.Close()

tracks, err := client.GetPlaylistItems(context.Background(), "playlistID")
if err != nil {
t.Error(err)
}
if tracks.Total != 4 {
t.Errorf("Got %d tracks, expected 47\n", tracks.Total)
}
if len(tracks.Items) == 0 {
t.Fatal("No tracks returned")
}

expected := "491- The Missing Middle"
actual := tracks.Items[0].Track.Episode.Name
if expected != actual {
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
}
added := tracks.Items[0].AddedAt
tm, err := time.Parse(TimestampLayout, added)
if err != nil {
t.Error(err)
}
if f := tm.Format(DateLayout); f != "2022-05-20" {
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
}

expected = "Typhoons"
actual = tracks.Items[2].Track.Track.Name
if expected != actual {
t.Errorf("Got '%s', expected '%s'\n", actual, expected)
}
added = tracks.Items[0].AddedAt
tm, err = time.Parse(TimestampLayout, added)
if err != nil {
t.Error(err)
}
if f := tm.Format(DateLayout); f != "2022-05-20" {
t.Errorf("Expected added at 2014-11-25, got %s\n", f)
}
}

func TestGetPlaylistItemsOverride(t *testing.T) {
var types string
client, server := testClientString(http.StatusForbidden, "", func(r *http.Request) {
types = r.URL.Query().Get("additional_types")
})
defer server.Close()

_, _ = client.GetPlaylistItems(context.Background(), "playlistID", AdditionalTypes(EpisodeAdditionalType))

if types != "episode" {
t.Errorf("Expected additional type episode, got %s\n", types)
}
}

func TestGetPlaylistItemsDefault(t *testing.T) {
var types string
client, server := testClientString(http.StatusForbidden, "", func(r *http.Request) {
types = r.URL.Query().Get("additional_types")
})
defer server.Close()

_, _ = client.GetPlaylistItems(context.Background(), "playlistID")

if types != "episode,track" {
t.Errorf("Expected additional type episode, got %s\n", types)
}
}

func TestUserFollowsPlaylist(t *testing.T) {
client, server := testClientString(http.StatusOK, `[ true, false ]`)
defer server.Close()
Expand All @@ -160,7 +290,7 @@ var newPlaylist = `
"collaborative": %t,
"description": "Test Description",
"external_urls": {
"spotify": "http://open.spotify.com/user/thelinmichael/playlist/7d2D2S200NyUE5KYs80PwO"
"spotify": "api.http://open.spotify.com/user/thelinmichael/playlist/7d2D2S200NyUE5KYs80PwO"
},
"followers": {
"href": null,
Expand All @@ -172,7 +302,7 @@ var newPlaylist = `
"name": "A New Playlist",
"owner": {
"external_urls": {
"spotify": "http://open.spotify.com/user/thelinmichael"
"spotify": "api.http://open.spotify.com/user/thelinmichael"
},
"href": "https://api.spotify.com/v1/users/thelinmichael",
"id": "thelinmichael",
Expand Down
23 changes: 23 additions & 0 deletions request_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package spotify
import (
"net/url"
"strconv"
"strings"
)

type RequestOption func(*requestOptions)
Expand Down Expand Up @@ -110,6 +111,28 @@ func Timerange(timerange Range) RequestOption {
}
}

type AdditionalType string

const (
EpisodeAdditionalType = "episode"
TrackAdditionalType = "track"
)

// AdditionalTypes is a list of item types that your client supports besides the default track type.
// Valid types are: EpisodeAdditionalType and TrackAdditionalType.
func AdditionalTypes(types ...AdditionalType) RequestOption {
strTypes := make([]string, len(types))
for i, t := range types {
strTypes[i] = string(t)
}

csv := strings.Join(strTypes, ",")

return func(o *requestOptions) {
o.urlParams.Set("additional_types", csv)
}
}

func processOptions(options ...RequestOption) requestOptions {
o := requestOptions{
urlParams: url.Values{},
Expand Down
Loading

0 comments on commit 13df4e2

Please sign in to comment.