Browse Source

Clean up utils.py to use database

Uses of the original library_stats dictionary in utils.py were either removed or commented out, replaced by the saving of models into the database.
master
Chris Shyi 7 years ago
parent
commit
797115e9cb
  1. 45
      spotifyvis/models.py
  2. 8
      spotifyvis/static/spotifyvis/scripts/test_db.js
  3. 11
      spotifyvis/templates/spotifyvis/test_db.html
  4. 113
      spotifyvis/utils.py
  5. 21
      spotifyvis/views.py

45
spotifyvis/models.py

@ -1,8 +1,7 @@
from django.db import models from django.db import models
# id's are 22 in length in examples but set to 30 for buffer # id's are 22 in length in examples but set to 30 for buffer
id_length=30
MAX_ID = 30
# Artist {{{ # # Artist {{{ #
class Artist(models.Model): class Artist(models.Model):
@ -10,7 +9,7 @@ class Artist(models.Model):
verbose_name = "Artist" verbose_name = "Artist"
verbose_name_plural = "Artists" verbose_name_plural = "Artists"
artist_id = models.CharField(primary_key=True, max_length=id_length)
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=20) genre = models.CharField(max_length=20)
@ -27,7 +26,7 @@ class User(models.Model):
verbose_name = "User" verbose_name = "User"
verbose_name_plural = "Users" verbose_name_plural = "Users"
user_id = models.CharField(primary_key=True, max_length=id_length) # 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 # username = models.CharField(max_length=30) # User's Spotify user name, if set
def __str__(self): def __str__(self):
@ -43,7 +42,7 @@ class Track(models.Model):
verbose_name = "Track" verbose_name = "Track"
verbose_name_plural = "Tracks" verbose_name_plural = "Tracks"
track_id = models.CharField(primary_key=True, max_length=id_length)
track_id = models.CharField(primary_key=True, max_length=MAX_ID)
# artist = models.ForeignKey(Artist, on_delete=models.CASCADE) # artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
artists = models.ManyToManyField(Artist, blank=True) artists = models.ManyToManyField(Artist, blank=True)
year = models.PositiveSmallIntegerField() year = models.PositiveSmallIntegerField()
@ -79,3 +78,39 @@ class AudioFeatures(models.Model):
return super(AudioFeatures, self).__str__() return super(AudioFeatures, self).__str__()
# }}} AudioFeatures # # }}} AudioFeatures #
'''
class UserAudioSummary(models.Model):
"""
Stores the summary of a user's audio preferences
"""
class Meta:
verbose_name = "AudioFeatures"
verbose_name_plural = "AudioFeatures"
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True,)
avg_acousticness = models.DecimalField(decimal_places=3, max_digits=3)
stdev_acousticness = models.DecimalField(decimal_places=3, max_digits=3)
avg_danceability = models.DecimalField(decimal_places=3, max_digits=3)
stdev_danceability = models.DecimalField(decimal_places=3, max_digits=3)
avg_energy = models.DecimalField(decimal_places=3, max_digits=3)
stdev_energy = models.DecimalField(decimal_places=3, max_digits=3)
avg_instrumentalness = models.DecimalField(decimal_places=3, max_digits=3)
stdev_instrumentalness = models.DecimalField(decimal_places=3, max_digits=3)
avg_loudness = models.DecimalField(decimal_places=3, max_digits=6)
stdev_loudness = models.DecimalField(decimal_places=3, max_digits=6)
avg_speechiness = models.DecimalField(decimal_places=3, max_digits=3)
stdev_speechiness = models.DecimalField(decimal_places=3, max_digits=3)
avg_tempo = models.DecimalField(decimal_places=3, max_digits=6)
stdev_tempo = models.DecimalField(decimal_places=3, max_digits=6)
avg_valence = models.DecimalField(decimal_places=3, max_digits=3)
stdev_valence = models.DecimalField(decimal_places=3, max_digits=3)
'''

8
spotifyvis/static/spotifyvis/scripts/test_db.js

@ -1,10 +1,4 @@
console.log("{{ user_id }}");
artist_data = JSON.parse('{{ artist_data }}');
artist_data.forEach(function(d) {
console.log(d.name, d.num_songs);
});
d3.json("{% url "get_artist_data" user_id %}", function(error, data) {
d3.json("{% url "get_artist_data", user_id %}").then(function(error, data) {
data.forEach(function(d) { data.forEach(function(d) {
console.log(d.name, d.num_songs); console.log(d.name, d.num_songs);
}); });

11
spotifyvis/templates/spotifyvis/test_db.html

@ -20,8 +20,13 @@
{% endfor %} {% endfor %}
</ul> </ul>
<pre> {% filter force_escape %} {% debug %} {% endfilter %} </pre> <pre> {% filter force_escape %} {% debug %} {% endfilter %} </pre>
<script src="http://d3js.org/d3.v3.js"></script>
{% load static %}
<script src="{% static "spotifyvis/scripts/test_db.js" %}"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
d3.json("{% url "get_artist_data" user_id %}").then(function(data) {
data.forEach(function(d) {
console.log(d.name, d.num_songs);
});
});
</script>
</body> </body>
</html> </html>

113
spotifyvis/utils.py

@ -1,5 +1,4 @@
# imports {{{ # # imports {{{ #
import requests import requests
import math import math
import pprint import pprint
@ -8,6 +7,7 @@ from .models import Artist, User, Track, AudioFeatures
from django.db.models import Count from django.db.models import Count
from django.http import JsonResponse from django.http import JsonResponse
from django.core import serializers from django.core import serializers
import json
# }}} imports # # }}} imports #
@ -32,7 +32,6 @@ def parse_library(headers, tracks, library_stats, user):
payload = {'limit': str(limit)} payload = {'limit': str(limit)}
# use two separate variables to track, because the average popularity also requires num_samples # use two separate variables to track, because the average popularity also requires num_samples
num_samples = 0 # number of actual track samples num_samples = 0 # number of actual track samples
feature_data_points = 0 # number of feature data analyses (some tracks do not have analyses available)
# 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):
@ -43,22 +42,27 @@ def parse_library(headers, tracks, library_stats, user):
# TODO: refactor the for loop body into helper function # TODO: refactor the for loop body into helper function
# iterate through each track # iterate through each track
for track_dict in saved_tracks_response['items']: for track_dict in saved_tracks_response['items']:
num_samples += 1
# update artist info before track so that Track object can reference # update artist info before track so that Track object can reference
# Artist object # Artist object
track_artists = [] track_artists = []
for artist_dict in track_dict['track']['artists']: for artist_dict in track_dict['track']['artists']:
increase_artist_count(headers, artist_dict['name'],
artist_dict['id'], library_stats)
track_artists.append(Artist.objects.get_or_create(
artist_obj, artist_created = Artist.objects.get_or_create(
artist_id=artist_dict['id'], artist_id=artist_dict['id'],
name=artist_dict['name'], name=artist_dict['name'],
)[0])
)
update_artist_genre(headers, artist_obj)
# get_or_create() returns a tuple (obj, created)
track_artists.append(artist_obj)
track_obj = save_track_obj(track_dict['track'], track_artists, user)
get_track_info(track_dict['track'], library_stats, num_samples)
audio_features_dict = get_audio_features(headers,
track_dict['track']['id'], track_obj)
track_obj, track_created = save_track_obj(track_dict['track'], track_artists, user)
# if a new track is not created, the associated audio feature does not need to be created again
if track_created:
save_audio_features(headers, track_dict['track']['id'], track_obj)
"""
TODO: Put this login in another function
# Audio analysis could be empty if not present in Spotify database
if len(audio_features_dict) != 0: if len(audio_features_dict) != 0:
# Track the number of audio analyses for calculating # Track the number of audio analyses for calculating
# audio feature averages and standard deviations on the fly # audio feature averages and standard deviations on the fly
@ -66,62 +70,55 @@ def parse_library(headers, tracks, library_stats, user):
for feature, feature_data in audio_features_dict.items(): for feature, feature_data in audio_features_dict.items():
update_audio_feature_stats(feature, feature_data, update_audio_feature_stats(feature, feature_data,
feature_data_points, library_stats) feature_data_points, library_stats)
"""
# calculates num_songs with offset + songs retrieved # calculates num_songs with offset + songs retrieved
library_stats['num_songs'] = offset + len(saved_tracks_response['items'])
offset += limit offset += limit
calculate_genres_from_artists(headers, library_stats)
# 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, 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.
: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.
:returns: (The created/retrieved Track object, created)
""" """
track_obj_query = Track.objects.filter(track_id__exact=track_dict['id'])
if len(track_obj_query) == 0:
new_track = Track.objects.create(
track_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'],
)
# print("pop/run: ", new_track.popularity, new_track.runtime)
# have to add artists and user after saving object since track needs to
# have ID before filling in m2m field
new_track, created = Track.objects.get_or_create(
track_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 ID before filling in m2m field
if created:
for artist in artists: for artist in artists:
new_track.artists.add(artist) new_track.artists.add(artist)
new_track.users.add(user) new_track.users.add(user)
new_track.save() new_track.save()
return new_track
elif len(track_obj_query) == 1:
return track_obj_query[0]
return new_track, created
# }}} save_track_obj # # }}} save_track_obj #
# get_audio_features {{{ # # get_audio_features {{{ #
def get_audio_features(headers, track_id, track):
"""Returns the audio features of a soundtrack
def save_audio_features(headers, track_id, track):
"""Creates and saves a new AudioFeatures object
Args: Args:
headers: headers containing the API token headers: headers containing the API token
track_id: the id of the soundtrack, needed to query the Spotify API track_id: the id of the soundtrack, needed to query the Spotify API
track: Track object to associate with the AudioFeatures object
track: Track object to associate with the new AudioFeatures object
Returns:
A dictionary with the features as its keys, if audio feature data is missing for the track,
an empty dictionary is returned.
""" """
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()
@ -141,7 +138,6 @@ def get_audio_features(headers, track_id, track):
setattr(audio_features_entry, key, val) setattr(audio_features_entry, key, val)
audio_features_entry.save() audio_features_entry.save()
return features_dict
# }}} get_audio_features # # }}} get_audio_features #
@ -300,30 +296,27 @@ def get_track_info(track_dict, library_stats, sample_size):
# }}} get_track_info # # }}} get_track_info #
# calculate_genres_from_artists {{{ #
# update_genres_from_artists {{{ #
def calculate_genres_from_artists(headers, library_stats):
"""Tallies up genre counts based on artists in library_stats.
def update_artist_genre(headers, artist_obj):
"""Updates the top genre for an artist by querying the Spotify API
:headers: For making the API call. :headers: For making the API call.
:library_stats: Dictionary containing the data mined from user's Spotify library
:artist_obj: the Artist object whose genre field will be updated
:returns: None :returns: None
""" """
for artist_entry in library_stats['artists'].values():
artist_response = requests.get('https://api.spotify.com/v1/artists/' + artist_entry['id'], headers=headers).json()
# increase each genre count by artist count
for genre in artist_response['genres']:
increase_nested_key('genres', genre, library_stats, artist_entry['count'])
# update genre for artist in database with top genre
Artist.objects.filter(artist_id=artist_entry['id']).update(genre=artist_response['genres'][0])
artist_response = requests.get('https://api.spotify.com/v1/artists/' + artist_obj.id, headers=headers).json()
# update genre for artist in database with top genre
artist_obj.update(genre=artist_response['genres'][0])
# }}} calculate_genres_from_artists # # }}} calculate_genres_from_artists #
# process_library_stats {{{ # # process_library_stats {{{ #
def process_library_stats(library_stats): def process_library_stats(library_stats):
"""Processes library_stats into format more suitable for D3 consumption """Processes library_stats into format more suitable for D3 consumption
@ -388,23 +381,3 @@ def get_genre_data(user):
# user_tracks = Track.objects.filter(users__exact=user) # user_tracks = Track.objects.filter(users__exact=user)
# for track in user_tracks: # for track in user_tracks:
# print(track.name) # print(track.name)
def get_artist_data(user_id):
"""Return artist data needed to create the graph for user.
:user_id: user ID for which to return the data for.
:returns: List of dicts containing counts for each artist.
"""
# TODO: not actual artists for user
# PICK UP: figure out how to pass data to D3/frontend
print(user_id)
# user = User.objects.get(user_id=user_id)
artist_counts = Artist.objects.annotate(num_songs=Count('track'))
processed_artist_data = [{'name': artist.name, 'num_songs': artist.num_songs} for artist in artist_counts]
# for artist in artist_counts:
# print(artist.name, artist.num_songs)
return JsonResponse(processed_artist_data, safe=False)
# return serializers.serialize('json', processed_artist_data)
# return processed_artist_data

21
spotifyvis/views.py

@ -10,9 +10,9 @@ import pprint
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
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.db.models import Count from django.db.models import Count
from .utils import parse_library, process_library_stats, get_artist_data
from .utils import parse_library, process_library_stats
from .models import User, Track, AudioFeatures, Artist from .models import User, Track, AudioFeatures, Artist
# }}} imports # # }}} imports #
@ -44,6 +44,7 @@ def generate_random_string(length):
# token_expired {{{ # # token_expired {{{ #
def token_expired(token_obtained_at, valid_for): def token_expired(token_obtained_at, valid_for):
"""Returns True if token expired, False if otherwise """Returns True if token expired, False if otherwise
@ -143,6 +144,8 @@ 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] user = User.objects.get_or_create(user_id=user_data_response['id'])[0]
context = { context = {
@ -174,8 +177,20 @@ def user_data(request):
def test_db(request): def test_db(request):
user_id = "polarbier" user_id = "polarbier"
context = { context = {
'artist_data': get_artist_data(user_id),
'user_id': user_id, 'user_id': user_id,
} }
# get_artist_data(user) # get_artist_data(user)
return render(request, 'spotifyvis/test_db.html', context) return render(request, 'spotifyvis/test_db.html', context)
def get_artist_data(request, user_id):
# TODO: not actual artists for user
# PICK UP: figure out how to pass data to D3/frontend
print(user_id)
# user = User.objects.get(user_id=user_id)
artist_counts = Artist.objects.annotate(num_songs=Count('track'))
processed_artist_data = [{'name': artist.name, 'num_songs': artist.num_songs} for artist in artist_counts]
# for artist in artist_counts:
# print(artist.name, artist.num_songs)
return JsonResponse(data=processed_artist_data, safe=False)
Loading…
Cancel
Save