Update Playlist Endpoints and Add Following Endpoints (#531)

* Update playlist endpoints to modern format

Deprecate user_playlist_* in favor of the following replacements:
* user_playlist_change_details -> playlist_change_details
* user_playlist_unfollow -> current_user_unfollow_playlist
* user_playlist_add_tracks -> playlist_add_tracks
* user_playlist_replace_tracks -> playlist_replace_tracks
* user_playlist_reorder_tracks -> playlist_reorder_tracks
* user_playlist_remove_all_occurrences_of_tracks -> playlist_remove_all_occurrences_of_tracks
* user_playlist_remove_specific_occurrences_of_tracks -> playlist_remove_specific_occurrences_of_tracks
* user_playlist_follow_playlist -> current_user_follow_playlist
* user_playlist_is_following -> playlist_is_following

* Add current_user_following_artists and current_user_following_users

* Update tests and examples

Resolve TODO in test_user_endpoints.py > SpotifyFollowApiTests.test_user_follows_and_unfollows_user

Use modern playlist endpoints (no username required) in tests and examples.

* Update changelog

* Deprecate playlist_tracks in favor of playlist_items

* Link deprecated functions to new functions and change tracks to items

* Fix references to playlist_tracks

* Change test_playlist_add_items as requested
This commit is contained in:
IdmFoundInHim 2020-07-11 06:23:18 -04:00 committed by GitHub
parent cc5e234a28
commit 88cf75b778
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 361 additions and 117 deletions

View File

@ -11,8 +11,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support to search multiple markets at once.
- Support to search all available Spotify markets.
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
- Support to test whether the current user is following certain
users or artists
- Proper replacements for all deprecated playlist endpoints
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
### Deprecated
- `user_playlist_change_details` in favor of `playlist_change_details`
- `user_playlist_unfollow` in favor of `current_user_unfollow_playlist`
- `user_playlist_add_tracks` in favor of `playlist_add_items`
- `user_playlist_replace_tracks` in favor of `playlist_replace_items`
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
- `user_playlist_remove_all_occurrences_of_tracks` in favor of
`playlist_remove_all_occurrences_of_items`
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of
`playlist_remove_specific_occurrences_of_items`
- `user_playlist_follow_playlist` in favor of
`current_user_follow_playlist`
- `user_playlist_is_following` in favor of `playlist_is_following`
- `playlist_tracks` in favor of `playlist_items`
## [2.13.0] - 2020-06-25

View File

@ -22,8 +22,7 @@ def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
user_id = sp.me()['id']
sp.user_playlist_add_tracks(user_id, args.playlist, args.tids)
sp.playlist_add_items(args.playlist, args.tids)
if __name__ == '__main__':

View File

@ -35,8 +35,7 @@ def get_args():
def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
sp.user_playlist_change_details(
args.username,
sp.playlist_change_details(
args.playlist,
name=args.name,
public=args.public or args.private,

View File

@ -10,20 +10,20 @@ sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
# load the first 100 songs
tracks = []
result = sp.playlist_tracks(PlaylistExample)
result = sp.playlist_items(PlaylistExample, additional_types=['track'])
tracks.extend(result['items'])
# if playlist is larger than 100 songs, continue loading it until end
while result['next']:
result = sp.next(result)
tracks.extend(result['items'])
result = sp.next(result)
tracks.extend(result['items'])
# remove all local songs
i = 0 # just for counting how many tracks are local
i = 0 # just for counting how many tracks are local
for item in tracks:
if item['is_local']:
tracks.remove(item)
i += 1
if item['is_local']:
tracks.remove(item)
i += 1
# print result

View File

@ -8,9 +8,10 @@ pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX'
offset = 0
while True:
response = sp.playlist_tracks(pl_id,
offset=offset,
fields='items.track.id,total')
response = sp.playlist_items(pl_id,
offset=offset,
fields='items.track.id,total',
additional_types=['track'])
pprint(response['items'])
offset = offset + len(response['items'])
print(offset, "/", response['total'])

View File

@ -22,8 +22,6 @@ else:
scope = 'playlist-modify-public'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
user_id = sp.me()['id']
results = sp.user_playlist_remove_specific_occurrences_of_tracks(
user_id, playlist_id, track_ids)
results = sp.playlist_remove_specific_occurrences_of_items(
playlist_id, track_ids)
pprint.pprint(results)

View File

@ -17,8 +17,6 @@ scope = 'playlist-modify-public'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
user_id = sp.me()['id']
results = sp.user_playlist_remove_all_occurrences_of_tracks(
user_id, playlist_id, track_ids)
results = sp.playlist_remove_all_occurrences_of_items(
playlist_id, track_ids)
pprint.pprint(results)

View File

@ -16,8 +16,6 @@ else:
scope = 'playlist-modify-public'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
user_id = sp.me()['id']
results = sp.user_playlist_replace_tracks(user_id, playlist_id, track_ids)
results = sp.playlist_replace_items(playlist_id, track_ids)
pprint.pprint(results)

View File

@ -607,6 +607,34 @@ class Spotify(object):
- additional_types - list of item types to return.
valid types are: track and episode
"""
warnings.warn(
"You should use `playlist_items(playlist_id, ...,"
"additional_types=('track',))` instead",
DeprecationWarning,
)
return self.playlist_items(playlist_id, fields, limit, offset,
market, additional_types)
def playlist_items(
self,
playlist_id,
fields=None,
limit=100,
offset=0,
market=None,
additional_types=("track", "episode")
):
""" Get full details of the tracks and episodes of a playlist.
Parameters:
- playlist_id - the id of the playlist
- fields - which fields to return
- limit - the maximum number of tracks to return
- offset - the index of the first track to return
- market - an ISO 3166-1 alpha-2 country code.
- additional_types - list of item types to return.
valid types are: track and episode
"""
plid = self._get_id("playlist", playlist_id)
return self._get(
"playlists/%s/tracks" % (plid),
@ -724,6 +752,10 @@ class Spotify(object):
collaborative=None,
description=None,
):
warnings.warn(
"You should use `playlist_change_details(playlist_id, ...)` instead",
DeprecationWarning,
)
""" Changes a playlist's name and/or public/private state
Parameters:
@ -735,18 +767,8 @@ class Spotify(object):
- description - optional description of the playlist
"""
data = {}
if isinstance(name, six.string_types):
data["name"] = name
if isinstance(public, bool):
data["public"] = public
if isinstance(collaborative, bool):
data["collaborative"] = collaborative
if isinstance(description, six.string_types):
data["description"] = description
return self._put(
"users/%s/playlists/%s" % (user, playlist_id), payload=data
)
return self.playlist_change_details(playlist_id, name, public,
collaborative, description)
def user_playlist_unfollow(self, user, playlist_id):
""" Unfollows (deletes) a playlist for a user
@ -755,13 +777,19 @@ class Spotify(object):
- user - the id of the user
- name - the name of the playlist
"""
return self._delete(
"users/%s/playlists/%s/followers" % (user, playlist_id)
warnings.warn(
"You should use `current_user_unfollow_playlist(playlist_id)` instead",
DeprecationWarning,
)
return self.current_user_unfollow_playlist(playlist_id)
def user_playlist_add_tracks(
self, user, playlist_id, tracks, position=None
):
warnings.warn(
"You should use `playlist_add_items(playlist_id, tracks)` instead",
DeprecationWarning,
)
""" Adds tracks to a playlist
Parameters:
@ -770,13 +798,7 @@ class Spotify(object):
- tracks - a list of track URIs, URLs or IDs
- position - the position to add the tracks
"""
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in tracks]
return self._post(
"users/%s/playlists/%s/tracks" % (user, plid),
payload=ftracks,
position=position,
)
return self.playlist_add_items(playlist_id, tracks, position)
def user_playlist_replace_tracks(self, user, playlist_id, tracks):
""" Replace all tracks in a playlist
@ -786,12 +808,11 @@ class Spotify(object):
- playlist_id - the id of the playlist
- tracks - the list of track ids to add to the playlist
"""
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in tracks]
payload = {"uris": ftracks}
return self._put(
"users/%s/playlists/%s/tracks" % (user, plid), payload=payload
warnings.warn(
"You should use `playlist_replace_items(playlist_id, tracks)` instead",
DeprecationWarning,
)
return self.playlist_replace_items(playlist_id, tracks)
def user_playlist_reorder_tracks(
self,
@ -814,17 +835,13 @@ class Spotify(object):
inserted
- snapshot_id - optional playlist's snapshot ID
"""
plid = self._get_id("playlist", playlist_id)
payload = {
"range_start": range_start,
"range_length": range_length,
"insert_before": insert_before,
}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._put(
"users/%s/playlists/%s/tracks" % (user, plid), payload=payload
warnings.warn(
"You should use `playlist_reorder_items(playlist_id, ...)` instead",
DeprecationWarning,
)
return self.playlist_reorder_items(playlist_id, range_start,
range_length, insert_before,
snapshot_id)
def user_playlist_remove_all_occurrences_of_tracks(
self, user, playlist_id, tracks, snapshot_id=None
@ -838,15 +855,14 @@ class Spotify(object):
- snapshot_id - optional id of the playlist snapshot
"""
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in tracks]
payload = {"tracks": [{"uri": track} for track in ftracks]}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete(
"users/%s/playlists/%s/tracks" % (user, plid), payload=payload
warnings.warn(
"You should use `playlist_remove_all_occurrences_of_items"
"(playlist_id, tracks)` instead",
DeprecationWarning,
)
return self.playlist_remove_all_occurrences_of_items(playlist_id,
tracks,
snapshot_id)
def user_playlist_remove_specific_occurrences_of_tracks(
self, user, playlist_id, tracks, snapshot_id=None
@ -863,7 +879,11 @@ class Spotify(object):
{ "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ]
- snapshot_id - optional id of the playlist snapshot
"""
warnings.warn(
"You should use `playlist_remove_specific_occurrences_of_items"
"(playlist_id, tracks)` instead",
DeprecationWarning,
)
plid = self._get_id("playlist", playlist_id)
ftracks = []
for tr in tracks:
@ -889,11 +909,11 @@ class Spotify(object):
- playlist_id - the id of the playlist
"""
return self._put(
"users/{}/playlists/{}/followers".format(
playlist_owner_id, playlist_id
)
warnings.warn(
"You should use `current_user_follow_playlist(playlist_id)` instead",
DeprecationWarning,
)
return self.current_user_follow_playlist(playlist_id)
def user_playlist_is_following(
self, playlist_owner_id, playlist_id, user_ids
@ -908,9 +928,196 @@ class Spotify(object):
if they follow the playlist. Maximum: 5 ids.
"""
endpoint = "users/{}/playlists/{}/followers/contains?ids={}"
warnings.warn(
"You should use `playlist_is_following(playlist_id, user_ids)` instead",
DeprecationWarning,
)
return self.playlist_is_following(playlist_id, user_ids)
def playlist_change_details(
self,
playlist_id,
name=None,
public=None,
collaborative=None,
description=None,
):
""" Changes a playlist's name and/or public/private state
Parameters:
- playlist_id - the id of the playlist
- name - optional name of the playlist
- public - optional is the playlist public
- collaborative - optional is the playlist collaborative
- description - optional description of the playlist
"""
data = {}
if isinstance(name, six.string_types):
data["name"] = name
if isinstance(public, bool):
data["public"] = public
if isinstance(collaborative, bool):
data["collaborative"] = collaborative
if isinstance(description, six.string_types):
data["description"] = description
return self._put(
"playlists/%s" % (playlist_id), payload=data
)
def current_user_unfollow_playlist(self, playlist_id):
""" Unfollows (deletes) a playlist for the current authenticated
user
Parameters:
- name - the name of the playlist
"""
return self._delete(
"playlists/%s/followers" % (playlist_id)
)
def playlist_add_items(
self, playlist_id, items, position=None
):
""" Adds tracks/episodes to a playlist
Parameters:
- playlist_id - the id of the playlist
- items - a list of track/episode URIs, URLs or IDs
- position - the position to add the tracks
"""
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items]
return self._post(
"playlists/%s/tracks" % (plid),
payload=ftracks,
position=position,
)
def playlist_replace_items(self, playlist_id, items):
""" Replace all tracks/episodes in a playlist
Parameters:
- playlist_id - the id of the playlist
- items - list of track/episode ids to comprise playlist
"""
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items]
payload = {"uris": ftracks}
return self._put(
"playlists/%s/tracks" % (plid), payload=payload
)
def playlist_reorder_items(
self,
playlist_id,
range_start,
insert_before,
range_length=1,
snapshot_id=None,
):
""" Reorder tracks in a playlist
Parameters:
- playlist_id - the id of the playlist
- range_start - the position of the first track to be reordered
- range_length - optional the number of tracks to be reordered
(default: 1)
- insert_before - the position where the tracks should be
inserted
- snapshot_id - optional playlist's snapshot ID
"""
plid = self._get_id("playlist", playlist_id)
payload = {
"range_start": range_start,
"range_length": range_length,
"insert_before": insert_before,
}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._put(
"playlists/%s/tracks" % (plid), payload=payload
)
def playlist_remove_all_occurrences_of_items(
self, playlist_id, items, snapshot_id=None
):
""" Removes all occurrences of the given tracks from the given playlist
Parameters:
- playlist_id - the id of the playlist
- items - list of track/episode ids to remove from the playlist
- snapshot_id - optional id of the playlist snapshot
"""
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items]
payload = {"tracks": [{"uri": track} for track in ftracks]}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete(
"playlists/%s/tracks" % (plid), payload=payload
)
def playlist_remove_specific_occurrences_of_items(
self, playlist_id, items, snapshot_id=None
):
""" Removes all occurrences of the given tracks from the given playlist
Parameters:
- playlist_id - the id of the playlist
- items - an array of objects containing Spotify URIs of the
tracks/episodes to remove with their current positions in
the playlist. For example:
[ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] },
{ "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ]
- snapshot_id - optional id of the playlist snapshot
"""
plid = self._get_id("playlist", playlist_id)
ftracks = []
for tr in items:
ftracks.append(
{
"uri": self._get_uri("track", tr["uri"]),
"positions": tr["positions"],
}
)
payload = {"tracks": ftracks}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete(
"playlists/%s/tracks" % (plid), payload=payload
)
def current_user_follow_playlist(self, playlist_id):
"""
Add the current authenticated user as a follower of a playlist.
Parameters:
- playlist_id - the id of the playlist
"""
return self._put(
"playlists/{}/followers".format(playlist_id)
)
def playlist_is_following(
self, playlist_id, user_ids
):
"""
Check to see if the given users are following the given playlist
Parameters:
- playlist_id - the id of the playlist
- user_ids - the ids of the users that you want to check to see
if they follow the playlist. Maximum: 5 ids.
"""
endpoint = "playlists/{}/followers/contains?ids={}"
return self._get(
endpoint.format(playlist_owner_id, playlist_id, ",".join(user_ids))
endpoint.format(playlist_id, ",".join(user_ids))
)
def me(self):
@ -954,6 +1161,36 @@ class Spotify(object):
"me/following", type="artist", limit=limit, after=after
)
def current_user_following_artists(self, ids=None):
""" Check if the current user is following certain artists
Returns list of booleans respective to ids
Parameters:
- ids - a list of artist URIs, URLs or IDs
"""
idlist = []
if ids is not None:
idlist = [self._get_id("artist", i) for i in ids]
return self._get(
"me/following/contains", ids=",".join(idlist), type="artist"
)
def current_user_following_users(self, ids=None):
""" Check if the current user is following certain artists
Returns list of booleans respective to ids
Parameters:
- ids - a list of user URIs, URLs or IDs
"""
idlist = []
if ids is not None:
idlist = [self._get_id("user", i) for i in ids]
return self._get(
"me/following/contains", ids=",".join(idlist), type="user"
)
def current_user_saved_tracks_delete(self, tracks=None):
""" Remove one or more tracks from the current user's
"Your Music" library.

View File

@ -53,15 +53,14 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
self.assertTrue('items' in playlists)
self.assertGreaterEqual(len(playlists['items']), 1)
def test_user_playlist_tracks(self):
def test_playlist_items(self):
playlists = self.spotify.user_playlists(self.username, limit=5)
self.assertTrue('items' in playlists)
for playlist in playlists['items']:
if playlist['uri'] != self.new_playlist_uri:
continue
user = playlist['owner']['id']
pid = playlist['id']
results = self.spotify.user_playlist_tracks(user, pid)
results = self.spotify.playlist_items(pid)
self.assertEqual(len(results['items']), 0)
def test_current_user_playlists(self):
@ -70,65 +69,60 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
self.assertGreaterEqual(len(playlists['items']), 1)
self.assertLessEqual(len(playlists['items']), 10)
def test_user_playlist_follow(self):
user_to_follow = 'plamere'
user_to_follow_id = '4erXB04MxwRAVqcUEpu30O'
self.spotify.user_playlist_follow_playlist(
user_to_follow, user_to_follow_id)
follows = self.spotify.user_playlist_is_following(
user_to_follow, user_to_follow_id, [self.username])
def test_current_user_follow_playlist(self):
playlist_to_follow_id = '4erXB04MxwRAVqcUEpu30O'
self.spotify.current_user_follow_playlist(playlist_to_follow_id)
follows = self.spotify.playlist_is_following(
playlist_to_follow_id, [self.username])
self.assertTrue(len(follows) == 1, 'proper follows length')
self.assertTrue(follows[0], 'is following')
self.spotify.user_playlist_unfollow(
user_to_follow, user_to_follow_id)
self.spotify.current_user_unfollow_playlist(playlist_to_follow_id)
follows = self.spotify.user_playlist_is_following(
user_to_follow, user_to_follow_id, [self.username])
follows = self.spotify.playlist_is_following(
playlist_to_follow_id, [self.username])
self.assertTrue(len(follows) == 1, 'proper follows length')
self.assertFalse(follows[0], 'is no longer following')
def test_user_playlist_replace_tracks(self):
def test_playlist_replace_items(self):
# add tracks to playlist
self.spotify.user_playlist_add_tracks(
self.username, self.new_playlist['id'], self.four_tracks)
playlist = self.spotify.user_playlist(self.username, self.new_playlist['id'])
self.spotify.playlist_add_items(
self.new_playlist['id'], self.four_tracks)
playlist = self.spotify.playlist(self.new_playlist['id'])
self.assertEqual(playlist['tracks']['total'], 4)
self.assertEqual(len(playlist['tracks']['items']), 4)
# replace with 3 other tracks
self.spotify.user_playlist_replace_tracks(self.username,
self.new_playlist['id'],
self.other_tracks)
playlist = self.spotify.user_playlist(self.username,
self.new_playlist['id'])
self.spotify.playlist_replace_items(self.new_playlist['id'],
self.other_tracks)
playlist = self.spotify.playlist(self.new_playlist['id'])
self.assertEqual(playlist['tracks']['total'], 3)
self.assertEqual(len(playlist['tracks']['items']), 3)
self.spotify.user_playlist_remove_all_occurrences_of_tracks(
self.username, playlist['id'], self.other_tracks)
playlist = self.spotify.user_playlist(self.username, self.new_playlist['id'])
self.spotify.playlist_remove_all_occurrences_of_items(
playlist['id'], self.other_tracks)
playlist = self.spotify.playlist(self.new_playlist['id'])
self.assertEqual(playlist["tracks"]["total"], 0)
def test_get_playlist_by_id(self):
pl = self.spotify.playlist(self.new_playlist['id'])
self.assertEqual(pl["tracks"]["total"], 0)
def test_playlist_add_tracks(self):
def test_playlist_add_items(self):
# add tracks to playlist
self.spotify.user_playlist_add_tracks(
self.username, self.new_playlist['id'], self.other_tracks)
playlist = self.spotify.user_playlist(self.username, self.new_playlist['id'])
self.assertEqual(playlist['tracks']['total'], 3)
self.assertEqual(len(playlist['tracks']['items']), 3)
self.spotify.playlist_add_items(
self.new_playlist['id'], self.other_tracks)
playlist = self.spotify.playlist_items(self.new_playlist['id'])
self.assertEqual(playlist['total'], 3)
self.assertEqual(len(playlist['items']), 3)
pl = self.spotify.playlist_tracks(self.new_playlist['id'], limit=2)
pl = self.spotify.playlist_items(self.new_playlist['id'], limit=2)
self.assertEqual(len(pl["items"]), 2)
self.spotify.user_playlist_remove_all_occurrences_of_tracks(
self.username, playlist['id'], self.other_tracks)
playlist = self.spotify.user_playlist(self.username, self.new_playlist['id'])
self.assertEqual(playlist["tracks"]["total"], 0)
self.spotify.playlist_remove_all_occurrences_of_items(
self.new_playlist['id'], self.other_tracks)
playlist = self.spotify.playlist_items(self.new_playlist['id'])
self.assertEqual(playlist["total"], 0)
def test_playlist_cover_image(self):
# Upload random dog image
@ -332,23 +326,24 @@ class SpotipyFollowApiTests(unittest.TestCase):
# Follow 2 more artists
artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"]
self.spotify.user_follow_artists(artists)
res = self.spotify.current_user_followed_artists()
self.assertEqual(res['artists']['total'], current_user_followed_artists + len(artists))
self.assertTrue(all(self.spotify.current_user_following_artists(artists)))
# Unfollow these 2 artists
self.spotify.user_unfollow_artists(artists)
self.assertFalse(any(self.spotify.current_user_following_artists(artists)))
res = self.spotify.current_user_followed_artists()
self.assertEqual(res['artists']['total'], current_user_followed_artists)
def test_user_follows_and_unfollows_user(self):
# TODO improve after implementing `me/following/contains`
users = ["11111204", "xlqeojt6n7on0j7coh9go8ifd"]
# Follow 2 more users
self.spotify.user_follow_users(users)
self.assertTrue(all(self.spotify.current_user_following_users(users)))
# Unfollow these 2 users
self.spotify.user_unfollow_users(users)
self.assertFalse(any(self.spotify.current_user_following_users(users)))
class SpotipyPlayerApiTests(unittest.TestCase):