From a35ee9715610113f72181c5bac25e67396b975e5 Mon Sep 17 00:00:00 2001 From: Whiddon Sibdhannie Date: Sat, 1 May 2021 17:23:05 -0400 Subject: [PATCH] Merge Spotify Client class into spotify app --- spotify_data/spotifyclient.py | 248 +++++++++++++++++++++++++++++++ spotify_data/tempoture-logo.jpeg | Bin 0 -> 19245 bytes 2 files changed, 248 insertions(+) create mode 100644 spotify_data/spotifyclient.py create mode 100644 spotify_data/tempoture-logo.jpeg diff --git a/spotify_data/spotifyclient.py b/spotify_data/spotifyclient.py new file mode 100644 index 0000000..65707d0 --- /dev/null +++ b/spotify_data/spotifyclient.py @@ -0,0 +1,248 @@ +import requests +import json +import base64 + +# GOAL: Create a cleaner SpotifyClient class to call from main app + + +RECENT_TRACKS_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played?limit=5' +PLAYLISTS_ENDPOINT = 'https://api.spotify.com/v1/me/playlists' +CREATE_PLAYLISTS_ENDPOINT = 'https://api.spotify.com/v1/users/{}/playlists' +MODIFY_TRACKS_PLAYLISTS_ENDPOINT = 'https://api.spotify.com/v1/playlists/{}/tracks' +CHANGE_PLAYLIST_COVER_ENDPOINT = 'https://api.spotify.com/v1/playlists/{}/images' +DEFAULT_PLAYLIST_COVER = 'tempoture-logo.jpeg' +DEFAULT_PLAYLIST_NAME = 'Your Tempoture Playlist' +DEFAULT_PLAYLIST_DESC = 'A custom-made playlist made by Tempoture for you!' + + + +class SpotifyClient: + def __init__(self, access_token, user_id): + """ + :param access_token (str): needed to fetch or push data from Spotify API + :param user_id (str): needed for playlist updates on user's account + + """ + self.access_token = access_token + self.user_id = user_id + + # https://developer.spotify.com/web-api/web-api-personalization-endpoints/get-recently-played + # Access Token requires scope: user-read-recently-played + def get_recent_tracks(self, after=None): + """ + :param after (int): optional timestamp in milliseconds + + Output: + trackURIs = list of Spotify track URIs from user's recently played + """ + + url = RECENT_TRACKS_ENDPOINT + if after is not None: + url += ('&after=' + str(after)) + response = self.get_api_request(url) + trackNames = [track["track"]["name"] for track in response.json()["items"]] + trackURIs = [track["track"]["uri"] for track in response.json()["items"]] + return trackURIs + + def get_current_user_playlists(self): + url = PLAYLISTS_ENDPOINT + response = self.get_api_request(self, url) + return response.json() + + # https://developer.spotify.com/documentation/web-api/reference/#endpoint-create-playlist + # Scopes needed: playlist-modify-public & playlist-modify-private + def create_playlist(self, name=None, desc=None): + """ + :param name (str): = optional argument for custom name of new playlist, will default to hardcoded string if none + :param desc (str): = optional argument for custom description of new playlist, will default to hardcoded string if none + + Output: + playlist_id (int): id of the newly created playlist + + """ + + url = CREATE_PLAYLISTS_ENDPOINT.format(self.user_id) + if name is None: + name = "Custom Tempoture Playlist" + + # Allow empty descriptions? + + data = json.dumps({ + "name": name, + "public": True, + "collaborative": False, + "description": desc + }) + response = self.post_api_request(url, data) + resp_json = response.json() + playlist_id = -1 + try: + playlist_id = resp_json["id"] + except Exception: + print("Status " + str(resp_json["error"]["status"]) + ": " + resp_json["error"]["message"]) + return playlist_id + + def change_playlist_cover(self, playlist_id, cover=None): + """ + Input: + :param playlist_id (int): Spotify playlist id + :param cover (str): optional argument for custom album cover photo, will default to hardcoded encoded string if none + + @todo: + Output: + a bool which determines if cover change went through successfully + + """ + url = CHANGE_PLAYLIST_COVER_ENDPOINT.format(playlist_id) + if (cover is None): + cover = DEFAULT_PLAYLIST_COVER + encoded_string = "" + with open(cover, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()) + file = encoded_string + response = requests.put( + url, + headers={ + "Content-Type": "image/jpeg", + "Authorization": f"Bearer {self.access_token}" + }, + data = encoded_string + ) + return True + + def add_playlist_tracks(self, playlist_id, track_uri_data): + """ + Input: + :param playlist_id (int): id of Spotify playlist being modified + :param track_uri_data (list): a list of Spotify track URI's , ex. ['spotify:track:xxxxxx', 'spotify:track:yyyyyyy'] + + Output: + snapshot_id = updated snapshot_id of the modified playlist (-1 if error occurred) + + """ + data = json.dumps(track_uri_data) + url = MODIFY_TRACKS_PLAYLISTS_ENDPOINT.format(playlist_id) + response = self.post_api_request(url, data) + resp_json = response.json() + snapshot_id = -1 + try: + snapshot_id = resp_json["snapshot_id"] + except Exception: + print("Status " + str(resp_json["error"]["status"]) + ": " + resp_json["error"]["message"]) + + return snapshot_id + + def remove_playlist_tracks(self, playlist_id, track_uri_data, snapshot_id): + """ + Input: + :param playlist_id (int): id of Spotify playlist being modified + :param track_uri_data (list): a list of Spotify track URIs to remove. Formatted ex: { "tracks": [{ "uri": "spotify:track:4iV5W9uYEdYUVa79Axb7Rh" },{ "uri": "spotify:track:1301WleyT98MSxVHPZCA6M" }] } + :param snapshot_id (int): (optional) playlist’s snapshot ID against which you want to make the changes + + @todo: + Output: + snapshot_id = updated snapshot_id of the modified playlist (-1 if error occurred) + + """ + data = json.dumps(track_uri_data) + url = MODIFY_TRACKS_PLAYLISTS_ENDPOINT.format(playlist_id) + response = self.delete_api_request(url, data) + resp_json = response.json() + snapshot_id = -1 + try: + snapshot_id = resp_json["snapshot_id"] + except Exception: + print("Status " + str(resp_json["error"]["status"]) + ": " + resp_json["error"]["message"]) + + print(resp_json) + return snapshot_id + + def get_api_request(self, url): + response = requests.get( + url, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}" + } + ) + return response + + def post_api_request(self, url, data): + response = requests.post( + url, + data=data, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}" + } + ) + return response + + def delete_api_request(self, url, data): + response = requests.delete( + url, + data=data, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}" + } + ) + return response + + # Create a new demo playlist and populate with your 5 most recent songs + def test_create_and_populate(self, name=None, desc=None): + """ + :param name (str): = optional argument for custom name of new playlist, will default to hardcoded string if none + :param desc (str): = optional argument for custom description of new playlist, will default to hardcoded string if none + + Output: + snapshot_id (int): a Spotify snapshot id which represents the specific playlist version + + """ + + playlist_id = self.create_playlist(name, desc) + self.change_playlist_cover(playlist_id) + url = MODIFY_TRACKS_PLAYLISTS_ENDPOINT.format(playlist_id) + track_uri_data = self.get_recent_tracks() + data = json.dumps(track_uri_data) + response = self.post_api_request(url, data) + resp_json = response.json() + snapshot_id = resp_json["snapshot_id"] + return snapshot_id + + + # Create a new demo playlist and populate with your 5 most recent songs + def create_tempoture_playlist(self, track_uri_data, name=None, desc=None, cover=None): + """ + Input: + :param track_uri_data (list): a list of Spotify track URI's , ex. ['spotify:track:xxxxxx', 'spotify:track:yyyyyyy'] + :param name (str): optional argument for custom name of new playlist, will default to hardcoded string if none + :param desc (str): optional argument for custom description of new playlist, will default to hardcoded string if none + :param cover (str): optional argument for custom album cover photo, will default to hardcoded encoded string if none + + Output: + playlist_id (int): id of newly created custom Spotify playlist + + """ + if name is None: + name = DEFAULT_PLAYLIST_NAME + if desc is None: + desc = DEFAULT_PLAYLIST_DESC + if cover is None: + cover = DEFAULT_PLAYLIST_COVER + + playlist_id = self.create_playlist(name, desc) + + if playlist_id == -1: + raise Exception("Could not create playlist.") + + if self.change_playlist_cover(playlist_id) == False: + raise Exception("Could not change playlist cover.") + + snapshot_id = self.add_playlist_tracks(playlist_id, track_uri_data) + + if snapshot_id == -1: + raise Exception("Could not add tracks to playlist.") + + return playlist_id + diff --git a/spotify_data/tempoture-logo.jpeg b/spotify_data/tempoture-logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..70d44dea7c1fd2f4785cd991da3ab9888964bb10 GIT binary patch literal 19245 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 bLu|y~s&lhR5_w*Rz~f|h%C_3t|KI-rj1+{P literal 0 HcmV?d00001