Browse Source

Merge branch 'apps' into loading-page

master
Kevin Mok 6 years ago
parent
commit
23126ba823
  1. 0
      api/__init__.py
  2. 0
      api/admin.py
  3. 4
      api/apps.py
  4. 22
      api/models.py
  5. 15
      api/urls.py
  6. 178
      api/utils.py
  7. 186
      api/views.py
  8. 0
      graphs/__init__.py
  9. 4
      graphs/apps.py
  10. 81
      graphs/static/graphs/scripts/artist_graph.js
  11. 0
      graphs/static/graphs/scripts/genre_graph.js
  12. 26
      graphs/templates/graphs/artist_graph.html
  13. 4
      graphs/templates/graphs/features_graphs.html
  14. 6
      graphs/templates/graphs/genre_graph.html
  15. 9
      graphs/templates/graphs/logged_in.html
  16. 13
      graphs/urls.py
  17. 8
      graphs/utils.py
  18. 42
      graphs/views.py
  19. 0
      login/__init__.py
  20. 4
      login/apps.py
  21. 22
      login/models.py
  22. 9
      login/templates/login/index.html
  23. 8
      login/templates/login/scan.html
  24. 12
      login/urls.py
  25. 10
      login/utils.py
  26. 142
      login/views.py
  27. 2
      manage.py
  28. 22
      musicvis/urls.py
  29. 6
      recreate-db.txt
  30. 15
      reset_db.sh
  31. 3
      reset_db.sql
  32. 250
      sample-track-obj.py
  33. 5
      spotifyvis/apps.py
  34. 11
      spotifyvis/settings.py
  35. 42
      spotifyvis/static/spotifyvis/scripts/index.js
  36. 67
      spotifyvis/tests.py
  37. 35
      spotifyvis/urls.py
  38. 283
      spotifyvis/views.py
  39. 2
      spotifyvis/wsgi.py
  40. 0
      static/css/dark_bg.css

0
musicvis/__init__.py → api/__init__.py

0
spotifyvis/admin.py → api/admin.py

4
api/apps.py

@ -0,0 +1,4 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

22
spotifyvis/models.py → api/models.py

@ -1,4 +1,5 @@
from django.db import models
from login.models import User
# id's are 22 in length in examples but set to 30 for buffer
MAX_ID = 30
@ -21,13 +22,12 @@ class Genre(models.Model):
# Artist {{{ #
class Artist(models.Model):
class Meta:
verbose_name = "Artist"
verbose_name_plural = "Artists"
artist_id = models.CharField(primary_key=True, max_length=MAX_ID)
id = models.CharField(primary_key=True, max_length=MAX_ID)
name = models.CharField(max_length=50)
genres = models.ManyToManyField(Genre, blank=True)
@ -36,22 +36,6 @@ class Artist(models.Model):
# }}} Artist #
# User {{{ #
class User(models.Model):
class Meta:
verbose_name = "User"
verbose_name_plural = "Users"
user_id = models.CharField(primary_key=True, max_length=MAX_ID) # the user's Spotify ID
user_secret = models.CharField(max_length=50, default='')
total_songs = models.PositiveIntegerField()
def __str__(self):
return self.user_id
# }}} User #
# Track {{{ #
class Track(models.Model):
@ -60,7 +44,7 @@ class Track(models.Model):
verbose_name = "Track"
verbose_name_plural = "Tracks"
track_id = models.CharField(primary_key=True, max_length=MAX_ID)
id = models.CharField(primary_key=True, max_length=MAX_ID)
# artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
artists = models.ManyToManyField(Artist, blank=True)
year = models.PositiveSmallIntegerField()

15
api/urls.py

@ -0,0 +1,15 @@
from django.urls import path, include
from .views import *
app_name = 'api'
urlpatterns = [
path('scan/<str:user_secret>', parse_library,
name='scan'),
path('user_artists/<str:user_secret>', get_artist_data,
name='get_artist_data'),
path('user_genres/<str:user_secret>', get_genre_data,
name='get_genre_data'),
path('audio_features/<str:audio_feature>/<str:user_secret>',
get_audio_feature_data, name='get_audio_feature_data'),
]

178
spotifyvis/utils.py → api/utils.py

@ -2,134 +2,36 @@
import requests
import math
import pprint
import os
import json
from .models import *
from django.db.models import Count, Q, F
from django.http import JsonResponse
from django.core import serializers
import json
from django.utils import timezone
from .models import *
from login.models import User
# }}} imports #
# global vars {{{ #
USER_TRACKS_LIMIT = 50
ARTIST_LIMIT = 50
FEATURES_LIMIT = 100
# ARTIST_LIMIT = 25
# FEATURES_LIMIT = 25
console_logging = True
# console_logging = False
artists_genre_processed = 0
features_processed = 0
# }}} global vars #
# parse_library {{{ #
def parse_library(headers, num_tracks, user):
"""Scans user's library for num_tracks and store the information in a database
:headers: For API call.
:num_tracks: Number of tracks to get from user's library (0 scans the entire
library).
:user: a User object representing the user whose library we are parsing
:returns: None
"""
offset = 0
payload = {'limit': str(USER_TRACKS_LIMIT)}
artist_genre_queue = []
features_queue = []
# create this obj so loop runs at least once
saved_tracks_response = [0]
# scan until reach num_tracks or no tracks left if scanning entire library
while (num_tracks == 0 or offset < num_tracks) and len(saved_tracks_response) > 0:
payload['offset'] = str(offset)
saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
headers=headers,
params=payload).json()['items']
if console_logging:
tracks_processed = 0
for track_dict in saved_tracks_response:
# add artists {{{ #
# update artist info before track so that Track object can reference
# Artist object
track_artists = []
for artist_dict in track_dict['track']['artists']:
artist_obj, artist_created = Artist.objects.get_or_create(
artist_id=artist_dict['id'],
name=artist_dict['name'],)
# only add/tally up artist genres if new
if artist_created:
artist_genre_queue.append(artist_obj)
if len(artist_genre_queue) == ARTIST_LIMIT:
add_artist_genres(headers, artist_genre_queue)
artist_genre_queue = []
track_artists.append(artist_obj)
# }}} add artists #
track_obj, track_created = save_track_obj(track_dict['track'],
track_artists, user)
# add audio features {{{ #
# if a new track is not created, the associated audio feature does
# not need to be created again
if track_created:
features_queue.append(track_obj)
if len(features_queue) == FEATURES_LIMIT:
get_audio_features(headers, features_queue)
features_queue = []
# }}} add audio features #
if console_logging:
tracks_processed += 1
print("Added track #{}: {} - {}".format(
offset + tracks_processed,
track_obj.artists.first(),
track_obj.name,
))
# calculates num_songs with offset + songs retrieved
offset += USER_TRACKS_LIMIT
# clean-up {{{ #
# update remaining artists without genres and songs without features if
# there are any
if len(artist_genre_queue) > 0:
add_artist_genres(headers, artist_genre_queue)
if len(features_queue) > 0:
get_audio_features(headers, features_queue)
# }}} clean-up #
update_track_genres(user)
# }}} parse_library #
# update_track_genres {{{ #
def update_track_genres(user):
"""Updates user's tracks with the most common genre associated with the
def update_track_genres(user_obj):
"""Updates user_obj's tracks with the most common genre associated with the
songs' artist(s).
:user: User object who's tracks are being updated.
:user_obj: User object who's tracks are being updated.
:returns: None
"""
tracks_processed = 0
user_tracks = Track.objects.filter(users__exact=user)
user_tracks = Track.objects.filter(users__exact=user_obj)
for track in user_tracks:
# just using this variable to save another call to db
track_artists = track.artists.all()
@ -146,7 +48,7 @@ def update_track_genres(user):
else undefined_genre_obj
track.save()
tracks_processed += 1
if console_logging:
print("Added '{}' as genre for song #{} - '{}'".format(
track.genre,
@ -158,34 +60,33 @@ def update_track_genres(user):
# save_track_obj {{{ #
def save_track_obj(track_dict, artists, user):
def save_track_obj(track_dict, artists, user_obj):
"""Make an entry in the database for this track if it doesn't exist already.
:track_dict: dictionary from the API call containing track information.
:artists: artists of the song, passed in as a list of Artist objects.
:top_genre: top genre associated with this track (see get_top_genre).
:user: User object for which this Track is to be associated with.
:user_obj: User object for which this Track is to be associated with.
:returns: (The created/retrieved Track object, created)
"""
track_query = Track.objects.filter(track_id__exact=track_dict['id'])
track_query = Track.objects.filter(id__exact=track_dict['id'])
if len(track_query) != 0:
return track_query[0], False
else:
new_track = Track.objects.create(
track_id=track_dict['id'],
id=track_dict['id'],
year=track_dict['album']['release_date'].split('-')[0],
popularity=int(track_dict['popularity']),
runtime=int(float(track_dict['duration_ms']) / 1000),
name=track_dict['name'],
)
# have to add artists and user after saving object since track needs to
# have to add artists and user_obj after saving object since track needs to
# have ID before filling in m2m field
for artist in artists:
new_track.artists.add(artist)
new_track.users.add(user)
new_track.users.add(user_obj)
new_track.save()
return new_track, True
@ -203,15 +104,16 @@ def get_audio_features(headers, track_objs):
:returns: None
"""
track_ids = str.join(",", [track_obj.track_id for track_obj in track_objs])
track_ids = str.join(",", [track_obj.id for track_obj in track_objs])
params = {'ids': track_ids}
features_response = requests.get("https://api.spotify.com/v1/audio-features",
headers=headers,
params={'ids': track_ids}
).json()['audio_features']
# pprint.pprint(features_response)
useless_keys = [ "key", "mode", "type", "liveness", "id", "uri", "track_href", "analysis_url", "time_signature", ]
useless_keys = [ "key", "mode", "type", "liveness", "id", "uri",
"track_href", "analysis_url", "time_signature", ]
for i in range(len(track_objs)):
if features_response[i] is not None:
# Data that we don't need
@ -222,11 +124,11 @@ def get_audio_features(headers, track_objs):
setattr(cur_features_obj, key, val)
cur_features_obj.save()
if console_logging:
global features_processed
features_processed += 1
print("Added features for song #{} - {}".format(
features_processed, track_objs[i].name))
if console_logging:
global features_processed
features_processed += 1
print("Added features for song #{} - {}".format(
features_processed, track_objs[i].name))
# }}} get_audio_features #
@ -264,8 +166,8 @@ def add_artist_genres(headers, artist_objs):
:returns: None
"""
artist_ids = str.join(",", [artist_obj.artist_id for artist_obj in artist_objs])
artist_ids = str.join(",", [artist_obj.id for artist_obj in artist_objs])
params = {'ids': artist_ids}
artists_response = requests.get('https://api.spotify.com/v1/artists/',
headers=headers,
params={'ids': artist_ids},
@ -318,3 +220,29 @@ def get_artists_in_genre(user, genre, max_songs):
return processed_artist_counts
# }}} get_artists_in_genre #
def get_user_header(user_obj):
"""Returns the authorization string needed to make an API call.
:user_obj: User to return the auth string for.
:returns: the authorization string used for the header in a Spotify API
call.
"""
seconds_elapsed = (timezone.now() -
user_obj.access_obtained_at).total_seconds()
if seconds_elapsed >= user_obj.access_expires_in:
req_body = {
'grant_type': 'refresh_token',
'refresh_token': user_obj.refresh_token,
'client_id': os.environ['SPOTIFY_CLIENT_ID'],
'client_secret': os.environ['SPOTIFY_CLIENT_SECRET']
}
token_response = requests.post('https://accounts.spotify.com/api/token',
data=req_body).json()
user_obj.access_token = token_response['access_token']
user_obj.access_expires_in = token_response['expires_in']
user_obj.save()
return {'Authorization': "Bearer " + user_obj.access_token}

186
api/views.py

@ -0,0 +1,186 @@
# imports {{{ #
import math
import random
import requests
import urllib
import secrets
import pprint
import string
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.db.models import Count, Q
from .utils import *
from .models import *
from login.models import User
from login.utils import get_user_context
# }}} imports #
USER_TRACKS_LIMIT = 50
ARTIST_LIMIT = 50
FEATURES_LIMIT = 100
# ARTIST_LIMIT = 25
# FEATURES_LIMIT = 25
TRACKS_TO_QUERY = 100
console_logging = True
# parse_library {{{ #
def parse_library(request, user_secret):
"""Scans user's library for num_tracks and store the information in a
database.
:user_secret: secret for User object who's library is being scanned.
:returns: None
"""
offset = 0
payload = {'limit': str(USER_TRACKS_LIMIT)}
artist_genre_queue = []
features_queue = []
user_obj = User.objects.get(secret=user_secret)
user_headers = get_user_header(user_obj)
# create this obj so loop runs at least once
saved_tracks_response = [0]
# scan until reach num_tracks or no tracks left if scanning entire library
while (TRACKS_TO_QUERY == 0 or offset < TRACKS_TO_QUERY) and len(saved_tracks_response) > 0:
payload['offset'] = str(offset)
saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
headers=user_headers,
params=payload).json()['items']
if console_logging:
tracks_processed = 0
for track_dict in saved_tracks_response:
# add artists {{{ #
# update artist info before track so that Track object can reference
# Artist object
track_artists = []
for artist_dict in track_dict['track']['artists']:
artist_obj, artist_created = Artist.objects.get_or_create(
id=artist_dict['id'],
name=artist_dict['name'],)
# only add/tally up artist genres if new
if artist_created:
artist_genre_queue.append(artist_obj)
if len(artist_genre_queue) == ARTIST_LIMIT:
add_artist_genres(user_headers, artist_genre_queue)
artist_genre_queue = []
track_artists.append(artist_obj)
# }}} add artists #
track_obj, track_created = save_track_obj(track_dict['track'],
track_artists, user_obj)
# add audio features {{{ #
# if a new track is not created, the associated audio feature does
# not need to be created again
if track_created:
features_queue.append(track_obj)
if len(features_queue) == FEATURES_LIMIT:
get_audio_features(user_headers, features_queue)
features_queue = []
# }}} add audio features #
if console_logging:
tracks_processed += 1
print("Added track #{}: {} - {}".format(
offset + tracks_processed,
track_obj.artists.first(),
track_obj.name,
))
# calculates num_songs with offset + songs retrieved
offset += USER_TRACKS_LIMIT
# clean-up {{{ #
# update remaining artists without genres and songs without features if
# there are any
if len(artist_genre_queue) > 0:
add_artist_genres(user_headers, artist_genre_queue)
if len(features_queue) > 0:
get_audio_features(user_headers, features_queue)
# }}} clean-up #
update_track_genres(user_obj)
return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
# }}} parse_library #
# get_artist_data {{{ #
def get_artist_data(request, user_secret):
"""Returns artist data as a JSON serialized list of dictionaries
The (key, value) pairs are (artist name, song count for said artist)
:param request: the HTTP request
:param user_secret: the user secret used for identification
:return: a JsonResponse
"""
user = User.objects.get(secret=user_secret)
artist_counts = Artist.objects.annotate(num_songs=Count('track',
filter=Q(track__users=user)))
processed_artist_counts = [{'name': artist.name, 'num_songs': artist.num_songs}
for artist in artist_counts]
pprint.pprint(processed_artist_counts)
return JsonResponse(data=processed_artist_counts, safe=False)
# }}} get_artist_data #
# get_audio_feature_data {{{ #
def get_audio_feature_data(request, audio_feature, user_secret):
"""Returns all data points for a given audio feature
Args:
request: the HTTP request
audio_feature: The audio feature to be queried
user_secret: client secret, used to identify the user
"""
user = User.objects.get(secret=user_secret)
user_tracks = Track.objects.filter(users=user)
response_payload = {
'data_points': [],
}
for track in user_tracks:
try:
audio_feature_obj = AudioFeatures.objects.get(track=track)
response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature))
except AudioFeatures.DoesNotExist:
continue
return JsonResponse(response_payload)
# }}} get_audio_feature_data #
# get_genre_data {{{ #
def get_genre_data(request, user_secret):
"""Return genre data needed to create the graph user.
TODO
"""
user = User.objects.get(secret=user_secret)
genre_counts = (Track.objects.filter(users__exact=user)
.values('genre')
.order_by('genre')
.annotate(num_songs=Count('genre'))
)
for genre_dict in genre_counts:
genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'],
genre_dict['num_songs'])
print("*** Genre Breakdown ***")
pprint.pprint(list(genre_counts))
return JsonResponse(data=list(genre_counts), safe=False)
# }}} get_genre_data #

0
graphs/__init__.py

4
graphs/apps.py

@ -0,0 +1,4 @@
from django.apps import AppConfig
class GraphsConfig(AppConfig):
name = 'graphs'

81
graphs/static/graphs/scripts/artist_graph.js

@ -0,0 +1,81 @@
/**
* Draws the artist count graph as a bubble chart, and appends it the a designated parent element
* @param artistData: the artist counts data as an array of objects, of the format {'name': artist name, 'num_songs': 50}
* @param parentElem: the DOM element to append the artist graph to (as a string)
*/
function drawArtistGraph(artistData, parentElem) {
let margin = {top: 20, right: 30, bottom: 30, left: 40};
let width = 960 - margin.right - margin.left;
let height = 540 - margin.top - margin.bottom;
let color = d3.scaleOrdinal(d3.schemeCategory10);
let bubble = d3.pack(artistData)
.size([width, height])
.padding(1.5);
let svg = d3.select(parentElem)
.append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("class", "bubble");
let nodes = d3.hierarchy(artistData)
.sum(function(d) { return d.num_songs; });
let node = svg.selectAll(".node")
.data(bubble(nodes).descendants())
.enter()
.filter(function(d) {
return !d.children;
})
.append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.append("title")
.text(function(d) {
return `${d.name}: ${d.num_songs}`;
});
node.append("circle")
.attr("r", function(d) {
return d.r;
})
.style("fill", function(d,i) {
return color(i);
});
// artist name text
node.append("text")
.attr("dy", ".2em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.name.substring(0, d.r / 3);
})
.attr("font-family", "sans-serif")
.attr("font-size", function(d){
return d.r/5;
})
.attr("fill", "white");
// artist song count text
node.append("text")
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.num_songs;
})
.attr("font-family", "Gill Sans", "Gill Sans MT")
.attr("font-size", function(d){
return d.r/5;
})
.attr("fill", "white");
d3.select(self.frameElement)
.style("height", height + "px");
}

0
spotifyvis/static/spotifyvis/scripts/genre_graph.js → graphs/static/graphs/scripts/genre_graph.js

26
graphs/templates/graphs/artist_graph.html

@ -0,0 +1,26 @@
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Artist Graphs</title>
</head>
<body>
<script src="https://d3js.org/d3.v5.js"></script>
<script src="{% static "graphs/scripts/artist_graph.js" %}"></script>
<script>
d3.json("{% url "api:get_artist_data" user_secret %}").then(function(data) {
for (let index = 0; index < data.length; index++) {
console.log(data[index].name);
console.log(data[index].num_songs);
}
// this is the data format needed for bubble charts
data = {
"children": data
};
drawArtistGraph(data, "body");
});
</script>
</body>
</html>

4
spotifyvis/templates/spotifyvis/audio_features.html → graphs/templates/graphs/features_graphs.html

@ -20,7 +20,6 @@
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<p>Logged in as {{ user_id }}</p>
<script src="https://d3js.org/d3.v5.js"></script>
<script type="text/javascript">
@ -37,6 +36,7 @@
* @return None
*/
function drawAudioFeatGraph(audioFeature, intervalEndPoints, parentElem) {
// TODO: Not hard code the dimensions?
let margin = {top: 20, right: 30, bottom: 30, left: 40};
let width = 480 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
@ -50,7 +50,7 @@
// define the vertical scaling function
let vScale = d3.scaleLinear().range([height, 0]);
d3.json(`/audio_features/${audioFeature}/{{ user_secret }}`)
d3.json(`/api/audio_features/${audioFeature}/{{ user_secret }}`)
.then(function(response) {
// categorize the data points
for (let dataPoint of response.data_points) {

6
spotifyvis/templates/spotifyvis/genre_graph.html → graphs/templates/graphs/genre_graph.html

@ -13,7 +13,7 @@
<title>Test DB Page</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}">
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
</head>
<!-- }}} header -->
@ -22,7 +22,7 @@
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/randomcolor/0.5.2/randomColor.min.js"></script>
{% load static %}
<script src="{% static "spotifyvis/scripts/genre_graph.js" %}"></script>
<script src="{% static "graphs/scripts/genre_graph.js" %}"></script>
<svg width="1920" height="740"></svg>
<script>
@ -38,7 +38,7 @@
var y = d3.scaleLinear()
.rangeRound([height, 0]);
d3.json("{% url "get_genre_data" user_secret %}").then(create_genre_graph);
d3.json("{% url "api:get_genre_data" user_secret %}").then(create_genre_graph);
</script>
</body>
</html>

9
spotifyvis/templates/spotifyvis/logged_in.html → graphs/templates/graphs/logged_in.html

@ -5,13 +5,16 @@
<meta charset="UTF-8">
<title>Logged In</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}">
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
</head>
<body>
<h1>{{ user_id }}'s Graphs</h1>
<a class="btn btn-primary" href="/audio_features/{{ user_secret }}"
<a class="btn btn-primary" href="{% url "graphs:display_audio_features" user_secret %}"
role="button">Audio Features</a>
<a class="btn btn-primary" href="{% url "display_genre_graph" user_secret %}"
<a class="btn btn-primary" href="{% url "graphs:display_genre_graph" user_secret %}"
role="button">Genres</a>
<a class="btn btn-primary" href="{% url "graphs:display_artist_graph" user_secret %}" role="button">
Artists
</a>
</body>
</html>

13
graphs/urls.py

@ -0,0 +1,13 @@
from django.urls import path, include
from .views import *
app_name = 'graphs'
urlpatterns = [
path('artists/<str:user_secret>', display_artist_graph,
name='display_artist_graph'),
path('genre/<str:user_secret>', display_genre_graph,
name='display_genre_graph'),
path('audio_features/<str:user_secret>', display_features_graphs,
name='display_audio_features'),
]

8
graphs/utils.py

@ -0,0 +1,8 @@
def get_secret_context(user_secret):
"""Return user_secret in context for graph pages.
:user_secret: User secret to put in context.
:returns: context with user secret.
"""
return { 'user_secret': user_secret, }

42
graphs/views.py

@ -0,0 +1,42 @@
# imports {{{ #
import math
import random
import requests
import os
import urllib
import secrets
import pprint
import string
from datetime import datetime
from django.shortcuts import render, redirect
from .utils import *
# }}} imports #
def display_artist_graph(request, user_secret):
"""Renders the artist data graph display page
:param request: the HTTP request
:param user_secret: the user secret used for identification
:return: render the artist data graph display page
"""
return render(request, "graphs/artist_graph.html",
get_secret_context(user_secret))
def display_genre_graph(request, user_secret):
return render(request, "graphs/genre_graph.html",
get_secret_context(user_secret))
def display_features_graphs(request, user_secret):
"""Renders the audio features page
:param request: the HTTP request
:param user_secret: user secret used for identification
:return: renders the audio features page
"""
return render(request, "graphs/features_graphs.html",
get_secret_context(user_secret))

0
login/__init__.py

4
login/apps.py

@ -0,0 +1,4 @@
from django.apps import AppConfig
class LoginConfig(AppConfig):
name = 'login'

22
login/models.py

@ -0,0 +1,22 @@
from django.db import models
# id's are 22 in length in examples but set to 30 for buffer
MAX_ID = 30
# saw tokens being about ~150 chars in length
TOKEN_LENGTH = 200
class User(models.Model):
class Meta:
verbose_name = "User"
verbose_name_plural = "Users"
# the user's Spotify ID
id = models.CharField(primary_key=True, max_length=MAX_ID)
secret = models.CharField(max_length=50, default='')
refresh_token = models.CharField(max_length=TOKEN_LENGTH)
access_token = models.CharField(max_length=TOKEN_LENGTH)
access_obtained_at = models.DateTimeField(auto_now=True)
access_expires_in = models.PositiveIntegerField()
def __str__(self):
return self.id

9
spotifyvis/templates/spotifyvis/index.html → login/templates/login/index.html

@ -4,7 +4,7 @@
<head>
<title>User Login</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}">
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
<style type="text/css">
.text-overflow {
overflow: hidden;
@ -20,12 +20,9 @@
<div class="container">
<div id="login">
<h1>spotify-lib-vis</h1>
<a href="/login" class="btn btn-primary">Scan Library</a>
<a href="{% url "admin_graphs" %}" class="btn btn-primary">Admin Graphs</a>
<a href="{% url 'login:spotify_login' %}" class="btn btn-primary">Login</a>
<a href="{% url 'login:admin_graphs' %}" class="btn btn-primary">Admin Graphs</a>
</div>
</div>
<script src="{% static 'spotifyvis/scripts/index.js' %}"></script>
</body>
</html>

8
spotifyvis/templates/spotifyvis/user_data.html → login/templates/login/scan.html

@ -10,12 +10,16 @@
<title>User Spotify Data</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<p>Logged in as {{ id }}</p>
<h1>Logged in as {{ user_id }}</h1>
<a href="{% url "api:scan" user_secret %}" class="btn btn-primary">
Scan Library
</a>
</body>
</html>

12
login/urls.py

@ -0,0 +1,12 @@
from django.urls import path, include
from .views import *
app_name = 'login'
urlpatterns = [
path('', index, name='index'),
path('spotify_login', spotify_login, name='spotify_login'),
path('callback', callback, name='callback'),
# path('user/<str:user_secret>', user_home, name='user_home'),
path('admin_graphs', admin_graphs, name='admin_graphs'),
]

10
login/utils.py

@ -0,0 +1,10 @@
from .models import User
def get_user_context(user_obj):
"""Get context for rendering with User's ID and secret.
:user_obj: User object to make context for.
:returns: context to pass back to HTML file.
"""
return { 'user_id': user_obj.id, 'user_secret': user_obj.secret, }

142
login/views.py

@ -0,0 +1,142 @@
# imports {{{ #
import math
import random
import requests
import os
import urllib
import secrets
import pprint
import string
from datetime import datetime
from django.shortcuts import render, redirect
from django.http import HttpResponseBadRequest
from .models import *
from .utils import *
# }}} imports #
TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
TRACKS_TO_QUERY = 200
# generate_random_string {{{ #
def generate_random_string(length):
"""Generates a random string of a certain length
Args:
length: the desired length of the randomized string
Returns:
A random string
"""
all_chars = string.ascii_letters + string.digits
rand_str = "".join(random.choice(all_chars) for _ in range(length))
return rand_str
# }}} generate_random_string #
# index {{{ #
# Create your views here.
def index(request):
return render(request, 'login/index.html')
# }}} index #
# spotify_login {{{ #
def spotify_login(request):
""" Step 1 in authorization flow: Have your application request
authorization; the user logs in and authorizes access.
"""
# use a randomly generated state string to prevent cross-site request forgery attacks
state_str = generate_random_string(16)
request.session['state_string'] = state_str
payload = {
'client_id': os.environ['SPOTIFY_CLIENT_ID'],
'response_type': 'code',
'redirect_uri': 'http://localhost:8000/login/callback',
'state': state_str,
'scope': 'user-library-read',
'show_dialog': False
}
params = urllib.parse.urlencode(payload) # turn the payload dict into a query string
authorize_url = "https://accounts.spotify.com/authorize/?{}".format(params)
return redirect(authorize_url)
# }}} spotify_login #
def callback(request):
""" Step 2 in authorization flow: Have your application request refresh and
access tokens; Spotify returns access and refresh tokens.
"""
# Attempt to retrieve the authorization code from the query string
try:
code = request.GET['code']
except KeyError:
return HttpResponseBadRequest("<h1>Problem with login</h1>")
payload = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'http://localhost:8000/login/callback',
'client_id': os.environ['SPOTIFY_CLIENT_ID'],
'client_secret': os.environ['SPOTIFY_CLIENT_SECRET'],
}
token_response = requests.post('https://accounts.spotify.com/api/token', data=payload).json()
user_obj = create_user(token_response['refresh_token'],
token_response['access_token'],
token_response['expires_in'])
return render(request, 'login/scan.html', get_user_context(user_obj))
# return redirect('user/' + user_obj.secret)
def create_user(refresh_token, access_token, access_expires_in):
"""Create a User object based on information returned from Step 2 (callback
function) of auth flow.
:refresh_token: Used to renew access tokens.
:access_token: Used in Spotify API calls.
:access_expires_in: How long the access token last in seconds.
:returns: The newly created User object.
"""
profile_response = requests.get('https://api.spotify.com/v1/me',
headers={'Authorization': "Bearer " + access_token}).json()
user_id = profile_response['id']
try:
user_obj = User.objects.get(id=user_id)
except User.DoesNotExist:
# Python docs recommends 32 bytes of randomness against brute
# force attacks
user_obj = User.objects.create(
id=user_id,
secret=secrets.token_urlsafe(32),
refresh_token=refresh_token,
access_token=access_token,
access_expires_in=access_expires_in,
)
return user_obj
# admin_graphs {{{ #
def admin_graphs(request):
"""TODO
"""
user_id = "polarbier"
# user_id = "chrisshyi13"
user_obj = User.objects.get(id=user_id)
return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
# }}} admin_graphs #

2
manage.py

@ -3,7 +3,7 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musicvis.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "spotifyvis.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

22
musicvis/urls.py

@ -1,22 +0,0 @@
"""musicdata URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', include('spotifyvis.urls')),
path('admin/', admin.site.urls),
]

6
recreate-db.txt

@ -1,6 +0,0 @@
sudo -u postgres psql
drop database spotifyvis;
create database spotifyvis with owner django;
\q
exit

15
reset_db.sh

@ -0,0 +1,15 @@
# check if in virtual environment
# https://stackoverflow.com/questions/15454174/how-can-a-shell-function-know-if-it-is-running-within-a-virtualenv/15454916
# python -c 'import sys; print(sys.real_prefix)' 2>/dev/null && INVENV=1 || INVENV=0
# INVENV=$(python -c 'import sys; print ("1" if hasattr(sys, "real_prefix") else "0")')
# if $INVENV is 1, then in virtualenv
# echo $INVENV
# if [ $INVENV -eq 1 ]; then
rm login/migrations/0* api/migrations/0* graphs/migrations/0*
sudo -u postgres psql -f reset_db.sql
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
# fi

3
reset_db.sql

@ -0,0 +1,3 @@
DROP DATABASE spotifyvis;
CREATE DATABASE spotifyvis;
GRANT ALL PRIVILEGES ON DATABASE spotifyvis TO django;

250
sample-track-obj.py

@ -1,250 +0,0 @@
{
'added_at':'2018-05-18T19:16:36Z',
'track':{
'album':{
'album_type':'single',
'artists':[
{
'external_urls':{
'spotify':'https://open.spotify.com/artist/64KEffDW9EtZ1y2vBYgq8T'
},
'href':'https://api.spotify.com/v1/artists/64KEffDW9EtZ1y2vBYgq8T',
'id':'64KEffDW9EtZ1y2vBYgq8T',
'name':'Marshmello',
'type':'artist',
'uri':'spotify:artist:64KEffDW9EtZ1y2vBYgq8T'
},
{
'external_urls':{
'spotify':'https://open.spotify.com/artist/5gCRApTajqwbnHHPbr2Fpi'
},
'href':'https://api.spotify.com/v1/artists/5gCRApTajqwbnHHPbr2Fpi',
'id':'5gCRApTajqwbnHHPbr2Fpi',
'name':'Juicy J',
'type':'artist',
'uri':'spotify:artist:5gCRApTajqwbnHHPbr2Fpi'
},
{
'external_urls':{
'spotify':'https://open.spotify.com/artist/4IWBUUAFIplrNtaOHcJPRM'
},
'href':'https://api.spotify.com/v1/artists/4IWBUUAFIplrNtaOHcJPRM',
'id':'4IWBUUAFIplrNtaOHcJPRM',
'name':'James Arthur',
'type':'artist',
'uri':'spotify:artist:4IWBUUAFIplrNtaOHcJPRM'
}
],
'available_markets':[
'AD',
'AR',
'AT',
'AU',
'BE',
'BG',
'BO',
'BR',
'CA',
'CH',
'CL',
'CO',
'CR',
'CY',
'CZ',
'DE',
'DK',
'DO',
'EC',
'EE',
'ES',
'FI',
'FR',
'GB',
'GR',
'GT',
'HK',
'HN',
'HU',
'ID',
'IE',
'IL',
'IS',
'IT',
'JP',
'LI',
'LT',
'LU',
'LV',
'MC',
'MT',
'MX',
'MY',
'NI',
'NL',
'NO',
'NZ',
'PA',
'PE',
'PH',
'PL',
'PT',
'PY',
'RO',
'SE',
'SG',
'SK',
'SV',
'TH',
'TR',
'TW',
'US',
'UY',
'VN',
'ZA'
],
'external_urls':{
'spotify':'https://open.spotify.com/album/6TvqOieExu0IJb9Q1gOoCz'
},
'href':'https://api.spotify.com/v1/albums/6TvqOieExu0IJb9Q1gOoCz',
'id':'6TvqOieExu0IJb9Q1gOoCz',
'images':[
{
'height':640,
'url':'https://i.scdn.co/image/b3556956b8e4881c85228ada91aa953e5c0458ef',
'width':640
},
{
'height':300,
'url':'https://i.scdn.co/image/d76072f5ca739466bd27f42f3356fa1a38c6a92d',
'width':300
},
{
'height':64,
'url':'https://i.scdn.co/image/bfd092dfa503566d9c9a3042f213fe02bed8a5cc',
'width':64
}
],
'name':'You Can Cry',
'release_date':'2018-05-04',
'release_date_precision':'day',
'type':'album',
'uri':'spotify:album:6TvqOieExu0IJb9Q1gOoCz'
},
'artists':[
{
'external_urls':{
'spotify':'https://open.spotify.com/artist/64KEffDW9EtZ1y2vBYgq8T'
},
'href':'https://api.spotify.com/v1/artists/64KEffDW9EtZ1y2vBYgq8T',
'id':'64KEffDW9EtZ1y2vBYgq8T',
'name':'Marshmello',
'type':'artist',
'uri':'spotify:artist:64KEffDW9EtZ1y2vBYgq8T'
},
{
'external_urls':{
'spotify':'https://open.spotify.com/artist/5gCRApTajqwbnHHPbr2Fpi'
},
'href':'https://api.spotify.com/v1/artists/5gCRApTajqwbnHHPbr2Fpi',
'id':'5gCRApTajqwbnHHPbr2Fpi',
'name':'Juicy J',
'type':'artist',
'uri':'spotify:artist:5gCRApTajqwbnHHPbr2Fpi'
},
{
'external_urls':{
'spotify':'https://open.spotify.com/artist/4IWBUUAFIplrNtaOHcJPRM'
},
'href':'https://api.spotify.com/v1/artists/4IWBUUAFIplrNtaOHcJPRM',
'id':'4IWBUUAFIplrNtaOHcJPRM',
'name':'James Arthur',
'type':'artist',
'uri':'spotify:artist:4IWBUUAFIplrNtaOHcJPRM'
}
],
'available_markets':[
'AD',
'AR',
'AT',
'AU',
'BE',
'BG',
'BO',
'BR',
'CA',
'CH',
'CL',
'CO',
'CR',
'CY',
'CZ',
'DE',
'DK',
'DO',
'EC',
'EE',
'ES',
'FI',
'FR',
'GB',
'GR',
'GT',
'HK',
'HN',
'HU',
'ID',
'IE',
'IL',
'IS',
'IT',
'JP',
'LI',
'LT',
'LU',
'LV',
'MC',
'MT',
'MX',
'MY',
'NI',
'NL',
'NO',
'NZ',
'PA',
'PE',
'PH',
'PL',
'PT',
'PY',
'RO',
'SE',
'SG',
'SK',
'SV',
'TH',
'TR',
'TW',
'US',
'UY',
'VN',
'ZA'
],
'disc_number':1,
'duration_ms':194533,
'explicit':False,
'external_ids':{
'isrc':'USQX91800946'
},
'external_urls':{
'spotify':'https://open.spotify.com/track/3ZbJMlEL4Kcme0ONRO7Slx'
},
'href':'https://api.spotify.com/v1/tracks/3ZbJMlEL4Kcme0ONRO7Slx',
'id':'3ZbJMlEL4Kcme0ONRO7Slx',
'name':'You Can Cry',
'popularity':81,
'preview_url':'https://p.scdn.co/mp3-preview/6c31f3dee18a1e7c452ce9b6948a6e04aa7629d6?cid=aefd4e45060d4f9ba5bea0f6e6d36359',
'track_number':1,
'type':'track',
'uri':'spotify:track:3ZbJMlEL4Kcme0ONRO7Slx'
}
}

5
spotifyvis/apps.py

@ -1,5 +0,0 @@
from django.apps import AppConfig
class SpotifyvisConfig(AppConfig):
name = 'spotifyvis'

11
musicvis/settings.py → spotifyvis/settings.py

@ -37,7 +37,9 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'spotifyvis.apps.SpotifyvisConfig',
'login.apps.LoginConfig',
'api.apps.ApiConfig',
'graphs.apps.GraphsConfig',
]
MIDDLEWARE = [
@ -50,7 +52,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'musicvis.urls'
ROOT_URLCONF = 'spotifyvis.urls'
TEMPLATES = [
{
@ -68,7 +70,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'musicvis.wsgi.application'
WSGI_APPLICATION = 'spotifyvis.wsgi.application'
# Database
@ -123,3 +125,6 @@ USE_TZ = True
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]

42
spotifyvis/static/spotifyvis/scripts/index.js

@ -1,42 +0,0 @@
document.getElementById("login-btn").addEventListener("click", function() {
let httpRequest = new XMLHttpRequest();
/*
* Handler for the response
*/
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
// hide the login button
document.getElementById('login').setAttribute("display", "none");
let responseData = JSON.parse(httpRequest.responseText);
let dataList = document.getElementById("data-list");
for (let key in responseData) {
let newLi = document.createElement("li");
let innerList = document.createElement("ul");
let dataLabel = document.createElement("li");
dataLabel.innerText = key;
let dataValue = document.createElement("li");
dataValue.innerText = responseData[key];
innerList.appendChild(dataLabel);
innerList.appendChild(dataValue);
newLi.appendChild(innerList);
dataList.appendChild(newLi);
}
} else {
alert("There was a problem with the login request, please try again!");
}
}
}
httpRequest.open('GET', '/login', true);
httpRequest.send();
});

67
spotifyvis/tests.py

@ -1,67 +0,0 @@
from django.test import TestCase
from .utils import update_std_dev
import math
# Create your tests here.
class UpdateStdDevTest(TestCase):
def test_two_data_points(self):
"""
tests if update_std_dev behaves correctly for two data points
"""
cur_mean = 5
cur_std_dev = 0
new_mean, new_std_dev = update_std_dev(cur_mean, cur_std_dev, 10, 2)
self.assertTrue(math.isclose(new_mean, 7.5, rel_tol=0.01))
self.assertTrue(math.isclose(new_std_dev, 3.5355, rel_tol=0.01))
def test_three_data_points(self):
"""
tests if update_std_dev behaves correctly for three data points
"""
cur_mean = 7.5
cur_std_dev = 3.5355
new_mean, new_std_dev = update_std_dev(cur_mean, cur_std_dev, 15, 3)
self.assertTrue(math.isclose(new_mean, 10, rel_tol=0.01))
self.assertTrue(math.isclose(new_std_dev, 5, rel_tol=0.01))
def test_four_data_points(self):
"""
tests if update_std_dev behaves correctly for four data points
"""
cur_mean = 10
cur_std_dev = 5
new_mean, new_std_dev = update_std_dev(cur_mean, cur_std_dev, 20, 4)
self.assertTrue(math.isclose(new_mean, 12.5, rel_tol=0.01))
self.assertTrue(math.isclose(new_std_dev, 6.455, rel_tol=0.01))
def test_five_data_points(self):
"""
tests if update_std_dev behaves correctly for five data points
"""
cur_mean = 12.5
cur_std_dev = 6.455
new_mean, new_std_dev = update_std_dev(cur_mean, cur_std_dev, 63, 5)
self.assertTrue(math.isclose(new_mean, 22.6, rel_tol=0.01))
self.assertTrue(math.isclose(new_std_dev, 23.2658, rel_tol=0.01))
def test_sixteen_data_points(self):
"""
tests if update_std_dev behaves correctly for sixteen data points
"""
cur_mean = 0.4441
cur_std_dev = 0.2855
new_mean, new_std_dev = update_std_dev(cur_mean, cur_std_dev, 0.7361, 16)
self.assertTrue(math.isclose(new_mean, 0.4624, rel_tol=0.01))
self.assertTrue(math.isclose(new_std_dev, 0.2853, rel_tol=0.01))

35
spotifyvis/urls.py

@ -1,19 +1,24 @@
from django.urls import path, include
from django.conf.urls import url
"""musicdata URL Configuration
from .views import *
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', index, name='index'),
path('login', login, name='login'),
path('callback', callback, name='callback'),
path('user_data', user_data, name='user_data'),
path('admin_graphs', admin_graphs, name='admin_graphs'),
path('user_artists/<str:user_id>', get_artist_data, name='get_artist_data'),
path('api/user_genres/<str:user_secret>', get_genre_data, name='get_genre_data'),
path('graphs/genre/<str:client_secret>', display_genre_graph,
name='display_genre_graph'),
path('audio_features/<str:client_secret>', audio_features, name='audio_features'),
path('audio_features/<str:audio_feature>/<str:client_secret>',
get_audio_feature_data, name='get_audio_feature_data'),
path('admin/', admin.site.urls),
path('login/', include('login.urls', namespace="login")),
path('api/', include('api.urls', namespace="api")),
path('graphs/', include('graphs.urls', namespace="graphs")),
]

283
spotifyvis/views.py

@ -1,283 +0,0 @@
# imports {{{ #
import math
import random
import requests
import os
import urllib
import secrets
import pprint
import string
from datetime import datetime
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.db.models import Count, Q
from .utils import parse_library, get_artists_in_genre, update_track_genres
from .models import User, Track, AudioFeatures, Artist
# }}} imports #
# global vars {{{ #
TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
TRACKS_TO_QUERY = 200
# TRACKS_TO_QUERY = 0
# }}} global vars #
# generate_random_string {{{ #
def generate_random_string(length):
"""Generates a random string of a certain length
Args:
length: the desired length of the randomized string
Returns:
A random string
"""
all_chars = string.ascii_letters + string.digits
rand_str = "".join(random.choice(all_chars) for _ in range(length))
return rand_str
# }}} generate_random_string #
# token_expired {{{ #
def token_expired(token_obtained_at, valid_for):
"""Returns True if token expired, False if otherwise
Args:
token_obtained_at: datetime object representing the date and time when the token was obtained
valid_for: the time duration for which the token is valid, in seconds
"""
time_elapsed = (datetime.today() - token_obtained_at).total_seconds()
return time_elapsed >= valid_for
# }}} token_expired #
# index {{{ #
# Create your views here.
def index(request):
return render(request, 'spotifyvis/index.html')
# }}} index #
# login {{{ #
# uses Authorization Code flow
def login(request):
# use a randomly generated state string to prevent cross-site request
# forgery attacks
state_str = generate_random_string(16)
request.session['state_string'] = state_str
payload = {
'client_id': os.environ['SPOTIFY_CLIENT_ID'],
'response_type': 'code',
'redirect_uri': 'http://localhost:8000/callback',
'state': state_str,
'scope': 'user-library-read',
'show_dialog': False
}
# turn the payload dict into a query string
params = urllib.parse.urlencode(payload)
authorize_url = "https://accounts.spotify.com/authorize/?{}".format(params)
return redirect(authorize_url)
# }}} login #
# callback {{{ #
def callback(request):
# Attempt to retrieve the authorization code from the query string
try:
code = request.GET['code']
except KeyError:
return HttpResponseBadRequest("<h1>Problem with login</h1>")
payload = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'http://localhost:8000/callback',
'client_id': os.environ['SPOTIFY_CLIENT_ID'],
'client_secret': os.environ['SPOTIFY_CLIENT_SECRET'],
}
response = requests.post('https://accounts.spotify.com/api/token', data=payload).json()
# despite its name, datetime.today() returns a datetime object, not a date object
# use datetime.strptime() to get a datetime object from a string
request.session['token_obtained_at'] = datetime.strftime(datetime.today(), TIME_FORMAT)
request.session['access_token'] = response['access_token']
request.session['refresh_token'] = response['refresh_token']
request.session['valid_for'] = response['expires_in']
# print(response)
return redirect('user_data')
# }}} callback #
# user_data {{{ #
def user_data(request):
# get user token {{{ #
token_obtained_at = datetime.strptime(request.session['token_obtained_at'], TIME_FORMAT)
valid_for = int(request.session['valid_for'])
if token_expired(token_obtained_at, valid_for):
req_body = {
'grant_type': 'refresh_token',
'refresh_token': request.session['refresh_token'],
'client_id': os.environ['SPOTIFY_CLIENT_ID'],
'client_secret': os.environ['SPOTIFY_CLIENT_SECRET']
}
refresh_token_response = requests.post('https://accounts.spotify.com/api/token', data=req_body).json()
request.session['access_token'] = refresh_token_response['access_token']
request.session['valid_for'] = refresh_token_response['expires_in']
# }}} get user token #
auth_token_str = "Bearer " + request.session['access_token']
headers = {
'Authorization': auth_token_str
}
user_data_response = requests.get('https://api.spotify.com/v1/me', headers = headers).json()
# store the user_id so it may be used to create model
request.session['user_id'] = user_data_response['id']
# create user obj {{{ #
total_songs = requests.get('https://api.spotify.com/v1/me/tracks',
headers=headers,
params={'limit': '1'}).json()['total']
try:
user = User.objects.get(user_id=user_data_response['id'])
except User.DoesNotExist:
# Python docs recommends 32 bytes of randomness against brute force attacks
user = User.objects.create(
user_id=user_data_response['id'],
user_secret=secrets.token_urlsafe(32),
total_songs=total_songs,
)
request.session['user_secret'] = user.user_secret
# }}} create user obj #
context = {
'user_id': user.user_id,
'user_secret': user.user_secret,
}
parse_library(headers, TRACKS_TO_QUERY, user)
return render(request, 'spotifyvis/logged_in.html', context)
# }}} user_data #
# admin_graphs {{{ #
def admin_graphs(request):
"""Redirect to logged in page as ourselves. For testing purposes.
"""
user_id = "polarbier"
# user_id = "chrisshyi13"
user_obj = User.objects.get(user_id=user_id)
context = {
'user_id': user_id,
'user_secret': user_obj.user_secret,
}
return render(request, 'spotifyvis/logged_in.html', context)
# }}} admin_graphs #
# get_artist_data {{{ #
def get_artist_data(request, user_secret):
"""TODO
"""
user = User.objects.get(user_id=user_secret)
artist_counts = Artist.objects.annotate(num_songs=Count('track',
filter=Q(track__users=user)))
processed_artist_counts = [{'name': artist.name,
'num_songs': artist.num_songs} for artist in artist_counts]
return JsonResponse(data=processed_artist_counts, safe=False)
# }}} get_artist_data #
# display_genre_graph {{{ #
def display_genre_graph(request, client_secret):
user = User.objects.get(user_secret=client_secret)
context = {
'user_secret': client_secret,
}
return render(request, "spotifyvis/genre_graph.html", context)
# }}} display_genre_graph #
# audio_features graph {{{ #
def audio_features(request, client_secret):
user = User.objects.get(user_secret=client_secret)
context = {
'user_id': user.user_id,
'user_secret': client_secret,
}
return render(request, "spotifyvis/audio_features.html", context)
# }}} audio_features graph #
# get_audio_feature_data {{{ #
def get_audio_feature_data(request, audio_feature, client_secret):
"""Returns all data points for a given audio feature
Args:
request: the HTTP request
audio_feature: The audio feature to be queried
client_secret: client secret, used to identify the user
"""
user = User.objects.get(user_secret=client_secret)
user_tracks = Track.objects.filter(users=user)
response_payload = {
'data_points': [],
}
for track in user_tracks:
try:
audio_feature_obj = AudioFeatures.objects.get(track=track)
response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature))
except AudioFeatures.DoesNotExist:
continue
return JsonResponse(response_payload)
# }}} get_audio_feature_data #
# get_genre_data {{{ #
def get_genre_data(request, user_secret):
"""Return genre data needed to create the graph user.
TODO
"""
user = User.objects.get(user_secret=user_secret)
genre_counts = (Track.objects.filter(users__exact=user)
.values('genre')
.order_by('genre')
.annotate(num_songs=Count('genre'))
)
for genre_dict in genre_counts:
genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'],
genre_dict['num_songs'])
print("*** Genre Breakdown ***")
pprint.pprint(list(genre_counts))
return JsonResponse(data=list(genre_counts), safe=False)
# }}} get_genre_data #

2
musicvis/wsgi.py → spotifyvis/wsgi.py

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musicvis.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "spotifyvis.settings")
application = get_wsgi_application()

0
spotifyvis/static/spotifyvis/css/dark_bg.css → static/css/dark_bg.css

Loading…
Cancel
Save