Browse Source

Add top genre to Track object

- merge from chris/audio_features branch
- fixed crashing when new artist doesn't have genre
- get genre/artist data using user secret
- removed punctuation from user secret
master
Kevin Mok 7 years ago
parent
commit
98b14b9000
  1. 1
      .gitignore
  2. 4
      spotifyvis/models.py
  3. 28
      spotifyvis/static/spotifyvis/scripts/user_data.js
  4. 21
      spotifyvis/templates/spotifyvis/test_db.html
  5. 11
      spotifyvis/templates/spotifyvis/user_data.html
  6. 3
      spotifyvis/urls.py
  7. 62
      spotifyvis/utils.py
  8. 103
      spotifyvis/views.py

1
.gitignore

@ -3,6 +3,7 @@ db.sqlite3
*.bak *.bak
.idea/ .idea/
.vscode/* .vscode/*
*/migrations/*
api-keys.sh api-keys.sh
Pipfile Pipfile

4
spotifyvis/models.py

@ -14,7 +14,6 @@ class Artist(models.Model):
artist_id = models.CharField(primary_key=True, max_length=MAX_ID) artist_id = models.CharField(primary_key=True, max_length=MAX_ID)
# unique since only storing one genre per artist right now # unique since only storing one genre per artist right now
name = models.CharField(unique=True, max_length=50) name = models.CharField(unique=True, max_length=50)
genre = models.CharField(max_length=30)
def __str__(self): def __str__(self):
return self.name return self.name
@ -29,7 +28,7 @@ class User(models.Model):
verbose_name_plural = "Users" verbose_name_plural = "Users"
user_id = models.CharField(primary_key=True, max_length=MAX_ID) # the user's Spotify ID user_id = models.CharField(primary_key=True, max_length=MAX_ID) # the user's Spotify ID
# username = models.CharField(max_length=30) # User's Spotify user name, if set
user_secret = models.CharField(max_length=30, default='')
def __str__(self): def __str__(self):
return self.user_id return self.user_id
@ -52,6 +51,7 @@ class Track(models.Model):
runtime = models.PositiveSmallIntegerField() runtime = models.PositiveSmallIntegerField()
name = models.CharField(max_length=150) name = models.CharField(max_length=150)
users = models.ManyToManyField(User, blank=True) users = models.ManyToManyField(User, blank=True)
genre = models.CharField(max_length=30)
def __str__(self): def __str__(self):
return self.name return self.name

28
spotifyvis/static/spotifyvis/scripts/user_data.js

@ -0,0 +1,28 @@
/**
* Retrieves data for a specific audio feature for a certain user
* @param audioFeature: the audio feature for which data will be retrieved
* @param clientSecret: the client secret, needed for security
*/
function getAudioFeatureData(audioFeature, userSecret) {
let httpRequest = new XMLHttpRequest();
/*
* Handler for the response
*/
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
let responseData = JSON.parse(httpRequest.responseText);
// TODO: The data points need to be plotted instead
for (let data of responseData.data_points) {
console.log(data);
}
} else {
alert("There was a problem with the login request, please try again!");
}
}
};
let queryString = `/audio_features/${audioFeature}/${userSecret}`;
httpRequest.open('GET', queryString, true);
httpRequest.send();
}

21
spotifyvis/templates/spotifyvis/test_db.html

@ -11,20 +11,19 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <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]-->
<ul>
{% for artist in artist_data %}
<li>{{ artist.name }} - {{ artist.num_songs }}</li>
{% endfor %}
</ul>
<pre> {% filter force_escape %} {% debug %} {% endfilter %} </pre>
<script src="https://d3js.org/d3.v5.min.js"></script> <script src="https://d3js.org/d3.v5.min.js"></script>
<script> <script>
d3.json("{% url "get_artist_data" user_id %}").then(function(data) {
/* console.log("artists");
d3.json("{% url "get_artist_data" user_secret %}").then(function(data) {
data.forEach(function(d) {
console.log(d.name, d.num_songs);
});
}); */
console.log("genres");
d3.json("{% url "get_genre_data" user_secret %}").then(function(data) {
data.forEach(function(d) { data.forEach(function(d) {
console.log(d.name, d.num_songs);
console.log(d.genre, d.num_songs);
}); });
}); });
</script> </script>

11
spotifyvis/templates/spotifyvis/user_data.html

@ -16,11 +16,10 @@
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p> <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
<![endif]--> <![endif]-->
<p>Logged in as {{ id }}</p> <p>Logged in as {{ id }}</p>
<h2>Display name {{ user_name }}</h2>
<ul>
{% for genre_name, genre_count in genre_dict.items %}
<li>{{ genre_name }} - {{ genre_count }}</li>
{% endfor %}
</ul>
<script src="{% static "spotifyvis/scripts/user_data.js" %}"></script>
<script>
sessionStorage.setItem('user_secret', "{{ user_secret }}");
getAudioFeatureData('instrumentalness', sessionStorage.getItem('user_secret'));
</script>
</body> </body>
</html> </html>

3
spotifyvis/urls.py

@ -10,4 +10,7 @@ urlpatterns = [
path('user_data', user_data, name='user_data'), path('user_data', user_data, name='user_data'),
path('test_db', test_db, name='test_db'), path('test_db', test_db, name='test_db'),
path('user_artists/<str:user_id>', get_artist_data, name='get_artist_data'), path('user_artists/<str:user_id>', 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:client_secret>',
get_audio_feature_data, name='get_audio_feature_data'),
] ]

62
spotifyvis/utils.py

@ -13,12 +13,12 @@ import json
# parse_library {{{ # # parse_library {{{ #
def parse_library(headers, tracks, library_stats, user):
def parse_library(headers, tracks, user):
"""Scans user's library for certain number of tracks to update library_stats with. """Scans user's library for certain number of tracks to update library_stats with.
:headers: For API call. :headers: For API call.
:tracks: Number of tracks to get from user's library. :tracks: Number of tracks to get from user's library.
:library_stats: Dictionary containing the data mined from user's library
:user: a User object representing the user whose library we are parsing :user: a User object representing the user whose library we are parsing
:returns: None :returns: None
@ -30,8 +30,6 @@ def parse_library(headers, tracks, library_stats, user):
# keeps track of point to get songs from # keeps track of point to get songs from
offset = 0 offset = 0
payload = {'limit': str(limit)} payload = {'limit': str(limit)}
# use two separate variables to track, because the average popularity also requires num_samples
num_samples = 0 # number of actual track samples
# iterate until hit requested num of tracks # iterate until hit requested num of tracks
for _ in range(0, tracks, limit): for _ in range(0, tracks, limit):
@ -51,11 +49,14 @@ def parse_library(headers, tracks, library_stats, user):
name=artist_dict['name'], name=artist_dict['name'],
) )
update_artist_genre(headers, artist_obj)
# update_artist_genre(headers, artist_obj)
# get_or_create() returns a tuple (obj, created) # get_or_create() returns a tuple (obj, created)
track_artists.append(artist_obj) track_artists.append(artist_obj)
track_obj, track_created = save_track_obj(track_dict['track'], track_artists, user)
top_genre = get_top_genre(headers,
track_dict['track']['artists'][0]['id'])
track_obj, track_created = save_track_obj(track_dict['track'],
track_artists, top_genre, user)
# if a new track is not created, the associated audio feature does not need to be created again # if a new track is not created, the associated audio feature does not need to be created again
if track_created: if track_created:
@ -73,23 +74,22 @@ def parse_library(headers, tracks, library_stats, user):
""" """
# calculates num_songs with offset + songs retrieved # calculates num_songs with offset + songs retrieved
offset += limit offset += limit
# calculate_genres_from_artists(headers, library_stats)
# pprint.pprint(library_stats) # pprint.pprint(library_stats)
# }}} parse_library # # }}} parse_library #
# save_track_obj {{{ # # save_track_obj {{{ #
def save_track_obj(track_dict, artists, user):
def save_track_obj(track_dict, artists, top_genre, user):
"""Make an entry in the database for this track if it doesn't exist already. """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. :track_dict: dictionary from the API call containing track information.
:artists: artists of the song, passed in as a list of Artist objects. :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: User object for which this Track is to be associated with.
:returns: (The created/retrieved Track object, created) :returns: (The created/retrieved Track object, created)
""" """
print(track_dict['name'])
track_query = Track.objects.filter(track_id__exact=track_dict['id']) track_query = Track.objects.filter(track_id__exact=track_dict['id'])
if len(track_query) != 0: if len(track_query) != 0:
return track_query[0], False return track_query[0], False
@ -100,6 +100,7 @@ def save_track_obj(track_dict, artists, user):
popularity=int(track_dict['popularity']), popularity=int(track_dict['popularity']),
runtime=int(float(track_dict['duration_ms']) / 1000), runtime=int(float(track_dict['duration_ms']) / 1000),
name=track_dict['name'], name=track_dict['name'],
genre=top_genre,
) )
# have to add artists and user after saving object since track needs to # have to add artists and user after saving object since track needs to
@ -127,7 +128,6 @@ def save_audio_features(headers, track_id, track):
response = requests.get("https://api.spotify.com/v1/audio-features/{}".format(track_id), headers = headers).json() response = requests.get("https://api.spotify.com/v1/audio-features/{}".format(track_id), headers = headers).json()
if 'error' in response: if 'error' in response:
return {} return {}
features_dict = {}
# Data that we don't need # Data that we don't need
useless_keys = [ useless_keys = [
@ -137,7 +137,6 @@ def save_audio_features(headers, track_id, track):
audio_features_entry.track = track audio_features_entry.track = track
for key, val in response.items(): for key, val in response.items():
if key not in useless_keys: if key not in useless_keys:
features_dict[key] = val
setattr(audio_features_entry, key, val) setattr(audio_features_entry, key, val)
audio_features_entry.save() audio_features_entry.save()
@ -299,8 +298,7 @@ def get_track_info(track_dict, library_stats, sample_size):
# }}} get_track_info # # }}} get_track_info #
# update_genres_from_artists {{{ #
# update_artist_genre {{{ #
def update_artist_genre(headers, artist_obj): def update_artist_genre(headers, artist_obj):
"""Updates the top genre for an artist by querying the Spotify API """Updates the top genre for an artist by querying the Spotify API
@ -313,10 +311,31 @@ def update_artist_genre(headers, artist_obj):
""" """
artist_response = requests.get('https://api.spotify.com/v1/artists/' + artist_obj.artist_id, headers=headers).json() artist_response = requests.get('https://api.spotify.com/v1/artists/' + artist_obj.artist_id, headers=headers).json()
# update genre for artist in database with top genre # update genre for artist in database with top genre
artist_obj.genre = artist_response['genres'][0]
artist_obj.save()
if len(artist_response['genres']) > 0:
artist_obj.genre = artist_response['genres'][0]
artist_obj.save()
# }}} #
# {{{ #
def get_top_genre(headers, top_artist_id):
"""Updates the top genre for a track by querying the Spotify API
:headers: For making the API call.
:top_artist: The first artist's (listed in the track) Spotify ID.
# }}} calculate_genres_from_artists #
:returns: The first genre listed for the top_artist.
"""
artist_response = requests.get('https://api.spotify.com/v1/artists/' +
top_artist_id, headers=headers).json()
if len(artist_response['genres']) > 0:
return artist_response['genres'][0]
else:
return "undefined"
# }}} #
# process_library_stats {{{ # # process_library_stats {{{ #
@ -374,14 +393,3 @@ def process_library_stats(library_stats):
# }}} process_library_stats # # }}} process_library_stats #
def get_genre_data(user):
"""Return genre data needed to create the graph user.
:user: User object for which to return the data for.
:returns: List of dicts containing counts for each genre.
"""
pass
# user_tracks = Track.objects.filter(users__exact=user)
# for track in user_tracks:
# print(track.name)

103
spotifyvis/views.py

@ -7,18 +7,19 @@ import os
import urllib import urllib
import json import json
import pprint import pprint
import string
from datetime import datetime from datetime import datetime
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.db.models import Count
from django.db.models import Count, Q
from .utils import parse_library, process_library_stats from .utils import parse_library, process_library_stats
from .models import User, Track, AudioFeatures, Artist from .models import User, Track, AudioFeatures, Artist
# }}} imports # # }}} imports #
TIME_FORMAT = '%Y-%m-%d-%H-%M-%S' TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
TRACKS_TO_QUERY = 5
TRACKS_TO_QUERY = 15
# generate_random_string {{{ # # generate_random_string {{{ #
@ -32,11 +33,8 @@ def generate_random_string(length):
Returns: Returns:
A random string A random string
""" """
rand_str = ""
possible_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for _ in range(length):
rand_str += possible_chars[random.randint(0, len(possible_chars) - 1)]
all_chars = string.ascii_letters + string.digits
rand_str = "".join(random.choice(all_chars) for _ in range(length))
return rand_str return rand_str
@ -105,7 +103,7 @@ def callback(request):
'client_secret': os.environ['SPOTIFY_CLIENT_SECRET'], 'client_secret': os.environ['SPOTIFY_CLIENT_SECRET'],
} }
response = requests.post('https://accounts.spotify.com/api/token', data = payload).json()
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 # despite its name, datetime.today() returns a datetime object, not a date object
# use datetime.strptime() to get a datetime object from a string # use datetime.strptime() to get a datetime object from a string
request.session['token_obtained_at'] = datetime.strftime(datetime.today(), TIME_FORMAT) request.session['token_obtained_at'] = datetime.strftime(datetime.today(), TIME_FORMAT)
@ -142,55 +140,86 @@ def user_data(request):
} }
user_data_response = requests.get('https://api.spotify.com/v1/me', headers = headers).json() user_data_response = requests.get('https://api.spotify.com/v1/me', headers = headers).json()
request.session['user_id'] = user_data_response['id'] # store the user_id so it may be used to create model
request.session['user_id'] = user_data_response['id'] # store the user_id so it may be used to create model
# request.session['user_name'] = user_data_response['display_name'] # request.session['user_name'] = user_data_response['display_name']
# get_or_create() returns a tuple (obj, created)
user = User.objects.get_or_create(user_id=user_data_response['id'])[0]
try:
user = User.objects.get(user_id=user_data_response['id'])
except User.DoesNotExist:
user = User(user_id=user_data_response['id'], user_secret=generate_random_string(30))
user.save()
context = { context = {
'user_name': user_data_response['display_name'],
'id': user_data_response['id'],
'id': user_data_response['id'],
'user_secret': user.user_secret,
} }
library_stats = {
"audio_features":{},
"genres":{},
"year_released":{},
"artists":{},
"num_songs": 0,
"popularity": {
"average": 0,
"std_dev": 0,
},
"total_runtime": 0
}
parse_library(headers, TRACKS_TO_QUERY, library_stats, user)
processed_library_stats = process_library_stats(library_stats)
# print("================================================")
# print("Processed data follows\n")
# pprint.pprint(processed_library_stats)
parse_library(headers, TRACKS_TO_QUERY, user)
return render(request, 'spotifyvis/user_data.html', context) return render(request, 'spotifyvis/user_data.html', context)
# }}} user_data # # }}} user_data #
# test_db {{{ #
def test_db(request): def test_db(request):
"""TODO """TODO
""" """
user_id = "polarbier" user_id = "polarbier"
context = { context = {
'user_id': user_id,
'user_secret': User.objects.get(user_id=user_id).user_secret,
} }
# get_artist_data(user)
return render(request, 'spotifyvis/test_db.html', context) return render(request, 'spotifyvis/test_db.html', context)
# }}} test_db #
def get_artist_data(request, user_id):
# get_artist_data {{{ #
def get_artist_data(request, user_secret):
"""TODO """TODO
""" """
# TODO: not actual artists for user
print(user_id)
# user = User.objects.get(user_id=user_id)
artist_counts = Artist.objects.annotate(num_songs=Count('track'))
user = User.objects.get(user_id=user_secret)
artist_counts = Artist.objects.annotate(num_songs=Count('track',
filter=Q(track__users=user)))
processed_artist_data = [{'name': artist.name, 'num_songs': artist.num_songs} for artist in artist_counts] processed_artist_data = [{'name': artist.name, 'num_songs': artist.num_songs} for artist in artist_counts]
return JsonResponse(data=processed_artist_data, safe=False) return JsonResponse(data=processed_artist_data, safe=False)
# }}} get_artist_data #
# 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:
audio_feature_obj = AudioFeatures.objects.get(track=track)
response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature))
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=user)
.values('genre')
.order_by('genre')
.annotate(num_songs=Count('genre'))
)
# pprint.pprint(genre_counts)
return JsonResponse(data=list(genre_counts), safe=False)
# }}} get_genre_data #
Loading…
Cancel
Save