Browse Source

Merge branch 'audio-features' of https://github.com/chrisshyi/spotify-lib-vis into database

master
Kevin Mok 7 years ago
parent
commit
35c8371cc7
  1. 5
      spotifyvis/admin.py
  2. 9
      spotifyvis/models.py
  3. 28
      spotifyvis/static/spotifyvis/scripts/user_data.js
  4. 141
      spotifyvis/templates/spotifyvis/audio_features.html
  5. 12
      spotifyvis/templates/spotifyvis/logged_in.html
  6. 1
      spotifyvis/urls.py
  7. 2
      spotifyvis/utils.py
  8. 20
      spotifyvis/views.py

5
spotifyvis/admin.py

@ -1,3 +1,8 @@
from django.contrib import admin
from .models import Track, Artist, AudioFeatures, User
# Register your models here.
admin.site.register(Track)
admin.site.register(Artist)
admin.site.register(AudioFeatures)
admin.site.register(User)

9
spotifyvis/models.py

@ -45,7 +45,7 @@ class User(models.Model):
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=30, default='')
user_secret = models.CharField(max_length=50, default='')
def __str__(self):
return self.user_id
@ -68,12 +68,15 @@ class Track(models.Model):
runtime = models.PositiveSmallIntegerField()
name = models.CharField(max_length=200)
users = models.ManyToManyField(User, blank=True)
# genre = models.CharField(max_length=30)
genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True,
null=True)
def __str__(self):
return self.name
track_str = "{}, genre: {}, artists: [".format(self.name, self.genre)
for artist in self.artists.all():
track_str += "{}, ".format(artist.name)
track_str += "]"
return track_str
# }}} Track #

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

@ -1,28 +0,0 @@
/**
* 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();
}

141
spotifyvis/templates/spotifyvis/audio_features.html

@ -0,0 +1,141 @@
{% load static %}
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>User Spotify Data</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.tick {
font-size: 15px;
}
</style>
</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 {{ user_id }}</p>
<script src="https://d3js.org/d3.v5.js"></script>
<script type="text/javascript">
/** Queries the backend for audio feature data, draws the bar chart
* illustrating the frequencies of values, and appends the chart to
* a designated parent element
*
* @param audioFeature: the name of the audio feature (string)
* @param intervalEndPoints: a sorted array of 5 real numbers defining the intervals (categories) of values,
* for example:
* [0, 0.25, 0.5, 0.75, 1.0] for instrumentalness would define ranges
* (0-0.25), (0.25-0.5), (0.5-0.75), (0.75-1.0)
* @param parentElem: the DOM element to append the graph to (a selector string)
* @return None
*/
function drawAudioFeatGraph(audioFeature, intervalEndPoints, parentElem) {
let margin = {top: 20, right: 30, bottom: 30, left: 40};
let width = 480 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
let featureData = {};
// Create the keys first in order
for (let index = 0; index < intervalEndPoints.length - 1; index++) {
let key = `${intervalEndPoints[index]} ~ ${intervalEndPoints[index + 1]}`;
featureData[key] = 0;
}
// define the vertical scaling function
let vScale = d3.scaleLinear().range([height, 0]);
d3.json(`/audio_features/${audioFeature}/{{ user_secret }}`)
.then(function(response) {
// categorize the data points
for (let dataPoint of response.data_points) {
dataPoint = parseFloat(dataPoint);
let index = intervalEndPoints.length - 2;
// find the index of the first element greater than dataPoint
while (dataPoint < intervalEndPoints[index]) {
index -= 1;
}
let key = `${intervalEndPoints[index]} ~ ${intervalEndPoints[index + 1]}`;
featureData[key] += 1;
}
let dataSet = Object.values(featureData);
let dataRanges = Object.keys(featureData); // Ranges of audio features, e.g. 0-0.25, 0.25-0.5, etc
let dataArr = [];
// turn the counts into an array of objects, e.g. {range: "0-0.25", counts: 5}
for (let i = 0; i < dataRanges.length; i++) {
dataArr.push({
range: dataRanges[i],
counts: featureData[dataRanges[i]]
});
}
vScale.domain([0, d3.max(dataSet)]).nice();
let hScale = d3.scaleBand().domain(dataRanges).rangeRound([0, width]).padding(0.5);
let xAxis = d3.axisBottom().scale(hScale);
let yAxis = d3.axisLeft().scale(vScale);
let featureSVG = d3.select(parentElem)
.append('svg').attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
let featureGraph = featureSVG.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.attr("fill", "teal");
featureGraph.selectAll(".bar")
.data(dataArr)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(d) { return hScale(d.range); })
.attr('y', function(d) { return vScale(d.counts); })
.attr("height", function(d) { return height - vScale(d.counts); })
.attr("width", hScale.bandwidth());
// function(d) { return hScale(d.range); }
featureGraph.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
featureGraph.append('g')
.attr('class', 'axis')
.call(yAxis);
featureSVG.append("text")
.attr('x', (width / 2))
.attr('y', (margin.top / 2))
.attr('text-anchor', 'middle')
.style('font-size', '14px')
.text(`${capFeatureStr(audioFeature)}`);
});
}
/**
* Returns the audio feature name string with the first letter capitalized
* @param audioFeature: the name of the audio feature
* @returns the audio feature name string with the first letter capitalized
*/
function capFeatureStr(audioFeature) {
return audioFeature.charAt(0).toUpperCase() + audioFeature.slice(1);
}
drawAudioFeatGraph("instrumentalness", [0, 0.25, 0.5, 0.75, 1.0], 'body');
drawAudioFeatGraph("valence", [0, 0.25, 0.5, 0.75, 1.0], 'body');
drawAudioFeatGraph("energy", [0, 0.25, 0.5, 0.75, 1.0], 'body');
drawAudioFeatGraph("tempo", [40, 80, 120, 160, 200], 'body');
drawAudioFeatGraph("danceability", [0, 0.25, 0.5, 0.75, 1.0], 'body');
drawAudioFeatGraph("acousticness", [0, 0.25, 0.5, 0.75, 1.0], 'body');
drawAudioFeatGraph("loudness", [-60, -45, -30, -15, 0], 'body');
drawAudioFeatGraph("speechiness", [0, 0.25, 0.5, 0.75, 1.0], 'body');
</script>
</body>
</html>

12
spotifyvis/templates/spotifyvis/logged_in.html

@ -0,0 +1,12 @@
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<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">
</head>
<body>
<a class="btn btn-primary" href="/audio_features/{{ user_secret }}" role="button">Audio Features</a>
</body>
</html>

1
spotifyvis/urls.py

@ -11,6 +11,7 @@ urlpatterns = [
path('test_db', test_db, name='test_db'),
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: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'),
]

2
spotifyvis/utils.py

@ -20,7 +20,7 @@ FEATURES_LIMIT = 100
# parse_library {{{ #
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 and store the information in a database
:headers: For API call.
:tracks: Number of tracks to get from user's library.

20
spotifyvis/views.py

@ -5,7 +5,7 @@ import random
import requests
import os
import urllib
import json
import secrets
import pprint
import string
from datetime import datetime
@ -117,6 +117,7 @@ def callback(request):
# user_data {{{ #
def user_data(request):
# get user token {{{ #
@ -152,18 +153,19 @@ def user_data(request):
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))
# Python docs recommends 32 bytes of randomness against brute force attacks
user = User(user_id=user_data_response['id'], user_secret=secrets.token_urlsafe(32))
request.session['user_secret'] = user.user_secret
user.save()
# }}} create user obj #
context = {
'id': user_data_response['id'],
'user_secret': user.user_secret,
}
parse_library(headers, TRACKS_TO_QUERY, user)
return render(request, 'spotifyvis/user_data.html', context)
return render(request, 'spotifyvis/logged_in.html', context)
# }}} user_data #
@ -173,8 +175,8 @@ def test_db(request):
"""TODO
"""
user_id = "polarbier"
# user_id = "chrisshyi13"
user_obj = User.objects.get(user_id=user_id)
# user_id = "35kxo00qqo9pd1comj6ylxjq7"
context = {
'user_secret': user_obj.user_secret,
}
@ -197,6 +199,14 @@ def get_artist_data(request, user_secret):
# }}} get_artist_data #
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)
# get_audio_feature_data {{{ #
def get_audio_feature_data(request, audio_feature, client_secret):

Loading…
Cancel
Save