Skip to content

Commit

Permalink
Back end work for #19, needs front end work
Browse files Browse the repository at this point in the history
  • Loading branch information
jhudis committed Jul 12, 2021
1 parent e41c4e3 commit 748872e
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 34 deletions.
5 changes: 3 additions & 2 deletions squadify/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from wtforms import StringField, BooleanField, validators
from wtforms.fields.html5 import URLField


Expand All @@ -9,4 +9,5 @@ class NewSquadForm(FlaskForm):

class AddPlaylistForm(FlaskForm):
user_name = StringField(validators=[validators.length(max=40), validators.input_required()])
playlist_link = URLField(validators=[validators.length(max=200), validators.input_required()])
playlist_link = URLField(validators=[validators.length(max=200)])
use_liked_songs = BooleanField()
34 changes: 23 additions & 11 deletions squadify/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
cache_handler = database.spotify_cache_handler()
auth_manager = SpotifyOAuth(cache_handler=cache_handler)
auth_manager = SpotifyOAuth(cache_handler=cache_handler, scope="playlist-modify-public")

if auth_manager.validate_token(cache_handler.get_cached_token()):
# User is not signed in
Expand All @@ -44,11 +44,11 @@ def wrapper(*args, **kwargs):
@app.get("/sign_in")
def sign_in():
auth_manager = SpotifyOAuth(
scope="playlist-modify-public", # Get permission to modify public playlists on behalf of the user
scope="playlist-modify-public,user-library-read", # Edit public playlist and view user library
cache_handler=database.spotify_cache_handler(),
show_dialog=True,
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI") + "/sign_in", # Have Spotify send us back here after signing in
state=request.args.get("dest", request.referrer or "/") # After being sent back here, go to this url
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI") + "/sign_in", # Have Spotify send us back here after signing in
state=request.args.get("dest", request.referrer or "/") # After being sent back here, go to this url
)

if not request.args.get("code"):
Expand Down Expand Up @@ -114,20 +114,32 @@ def new_squad(spotify_api):


# Add a playlist to an existing squad
# Note: We know that playlist_link is a valid URL via form validation, but we
# Note 1: We know that playlist_link is a valid URL via form validation, but we
# don't know if the playlist_id we get out of it points to a valid playlist
# until it's time to compile the collab
# Note 2: A user can opt to add their liked songs as a playlist, but since
# Spotify doesn't treat liked songs as a playlist, we must make a playlist on
# their account and add their liked songs to that playlist
@app.post("/squads/<squad:squad>/add_playlist")
def add_playlist(squad):
@authenticate(required=False)
def add_playlist(spotify_api, signed_in, squad):
add_playlist_form = AddPlaylistForm()

# Add a playlist only if the user submitted the playlist info
if add_playlist_form.validate_on_submit():
playlist_id = urlparse(add_playlist_form.playlist_link.data).path.split("/")[-1]
if add_playlist_form.use_liked_songs.data:
# Use liked songs
if not signed_in:
# Not allowed for logged out users, redirect back to squad page
return redirect(f"/squads/{squad['squad_id']}")
playlist_id = spotify_api.clone_liked_songs()
else:
# Use provided playlist link
playlist_id = urlparse(add_playlist_form.playlist_link.data).path.split("/")[-1]
database.add_playlist_to_squad(squad["squad_id"], playlist_id, add_playlist_form.user_name.data)

# Regardless of whether or not a playlist was added, redirect back to the squad page
return redirect(f"/squads/{squad['squad_id']}")
return redirect(f"/squads/{squad['squad_id']}")


# Delete a playlist from an existing squad
Expand All @@ -146,15 +158,15 @@ def compile_squad(spotify_api, squad):
# Transform playlists list and filter out invalid playlist ids
playlists = [(playlist["user_name"], playlist["playlist_id"]) for playlist in squad["playlists"]]
playlists = filter(lambda playlist: spotify_api.is_valid_playlist_id(playlist[1]), playlists)
playlists = [Playlist(name, spotify_api.get_tracks(id)) for name, id in playlists]
playlists = [Playlist(name, spotify_api.get_playlist_tracks(id)) for name, id in playlists]

# Do nothing if the squad has no valid playlists
if len(playlists) == 0:
return redirect(f"/squads/{squad['squad_id']}")

# Build collab
# Build a collaborative playlist from this squad
collab = CollabBuilder(playlists).build()
collab_id = spotify_api.publish_collab(collab, squad["squad_name"])
collab_id = spotify_api.create_playlist_with_tracks(squad["squad_name"], collab)

return render_template(
"compile-squad.html",
Expand Down
68 changes: 47 additions & 21 deletions squadify/spotify_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,66 @@


TRACK_PULL_LIMIT = 100 # Number of tracks the Spotify API lets you query at once
TRACK_PUSH_LIMIT = 20 # Number of tracks the Spotify API lets you add at once
TRACK_PUSH_LIMIT = 100 # Number of tracks the Spotify API lets you add at once
LIKED_SONGS_PULL_LIMIT = 50 # Number of liked songs the Spotify API lets you query at once
LIKED_SONGS_PLAYLIST_NAME = "User's Liked Songs"


# Added functionality on top of the Spotipy module
class SpotifyAPI(Spotify):

# Return a list of all the tracks from a playlist
def get_tracks(self, playlist_id):
num_tracks = self.playlist(playlist_id, fields=["tracks(total)"])["tracks"]["total"]
tracks = []
for i in range(math.ceil(num_tracks / TRACK_PULL_LIMIT)):
playlist_items = self.playlist_items(playlist_id, offset=i * TRACK_PULL_LIMIT)
for item in playlist_items["items"]:
# Don't add tracks with missing data
if item["track"] == None or item["track"]["id"] == None:
continue
tracks.append(Track(item["track"]))
def get_playlist_tracks(self, playlist_id):
return self.__pull_tracks(self.playlist_items(playlist_id, limit=TRACK_PULL_LIMIT))


# Create a new playlist contianing the given tracks to this user's account
# Returns the ID of the new playlist
def create_playlist_with_tracks(self, playlist_name, tracks):
playlist_id = self.__create_playlist(playlist_name)
self.__push_tracks(playlist_id, tracks)
return playlist_id


# Copy this user's Liked Songs list to a playlist
# Returns the ID of the new playlist
def clone_liked_songs(self):
tracks = self.__pull_tracks(self.current_user_saved_tracks(limit=LIKED_SONGS_PULL_LIMIT))
return self.create_playlist_with_tracks(LIKED_SONGS_PLAYLIST_NAME, tracks)


# Pull tracks using any initial result from a function that supports pagination
def __pull_tracks(self, result):
# Iterate through paginated results
items = result["items"]
while result["next"]:
result = self.next(result)
items.extend(result["items"])

# Transform items into tracks and filter out ones with missing data
items = filter(lambda item: item["track"] and item["track"]["id"], items)
tracks = [Track(item["track"]) for item in items]

return tracks


# Make collab in leader's account and return its Spotify ID
def publish_collab(self, collab, collab_name):
user_id = self.current_user()["id"]
collab_id = self.user_playlist_create(user_id, collab_name)["id"]
tracks = [track.id for track in collab]
for i in range(0, len(tracks), TRACK_PUSH_LIMIT):
tracks_subset = tracks[i : min(i + TRACK_PUSH_LIMIT, len(tracks))]
self.user_playlist_add_tracks(user_id, collab_id, tracks_subset)
return collab_id
# Push the given tracks to the playlist with the given ID
def __push_tracks(self, playlist_id, tracks):
track_ids = [track.id for track in tracks]
for i in range(0, len(track_ids), TRACK_PUSH_LIMIT):
track_ids_sublist = track_ids[i : min(i + TRACK_PUSH_LIMIT, len(track_ids))]
self.playlist_add_items(playlist_id, track_ids_sublist)


# Add a new playlist with the given name to this user's account
def __create_playlist(self, playlist_name):
return self.user_playlist_create(self.current_user()["id"], playlist_name)["id"]


# Returns whether this is the ID of a valid playlist
def is_valid_playlist_id(self, playlist_id):
try:
self.playlist(playlist_id)
return True
except:
return False
return False
1 change: 1 addition & 0 deletions squadify/templates/squad-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
{{ add_playlist_form.csrf_token }}
{{ add_playlist_form.user_name(class="button", placeholder="Your Name") }}
{{ add_playlist_form.playlist_link(class="button", placeholder="Playlist Link") }}
{{ add_playlist_form.use_liked_songs(class="checkbox") }}
<input class="button max-w-max" type="submit" value="Add Your Playlist">
</form>
</div>
Expand Down

0 comments on commit 748872e

Please sign in to comment.