Split spotifyvis code into different apps (#47)
Server is able to start, but none of the apps are linked together yet.
This commit is contained in:
4
api/apps.py
Normal file
4
api/apps.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
name = 'api'
|
||||||
12
api/urls.py
Normal file
12
api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
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'),
|
||||||
|
]
|
||||||
86
api/views.py
Normal file
86
api/views.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# imports {{{ #
|
||||||
|
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import urllib
|
||||||
|
import secrets
|
||||||
|
import pprint
|
||||||
|
import string
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.http import 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 #
|
||||||
|
|
||||||
|
TRACKS_TO_QUERY = 200
|
||||||
|
|
||||||
|
# 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(user_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]
|
||||||
|
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(user_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(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 #
|
||||||
4
graphs/apps.py
Normal file
4
graphs/apps.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class GraphsConfig(AppConfig):
|
||||||
|
name = 'graphs'
|
||||||
104
graphs/models.py
Normal file
104
graphs/models.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# id's are 22 in length in examples but set to 30 for buffer
|
||||||
|
MAX_ID = 30
|
||||||
|
|
||||||
|
# Genre {{{ #
|
||||||
|
|
||||||
|
class Genre(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Genre"
|
||||||
|
verbose_name_plural = "Genres"
|
||||||
|
|
||||||
|
name = models.CharField(primary_key=True, max_length=50)
|
||||||
|
num_songs = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
# }}} Genre #
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
# unique since only storing one genre per artist right now
|
||||||
|
name = models.CharField(unique=True, max_length=50)
|
||||||
|
genres = models.ManyToManyField(Genre, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
# }}} 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='')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user_id
|
||||||
|
|
||||||
|
# }}} User #
|
||||||
|
|
||||||
|
# Track {{{ #
|
||||||
|
|
||||||
|
class Track(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Track"
|
||||||
|
verbose_name_plural = "Tracks"
|
||||||
|
|
||||||
|
track_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()
|
||||||
|
popularity = models.PositiveSmallIntegerField()
|
||||||
|
runtime = models.PositiveSmallIntegerField()
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
users = models.ManyToManyField(User, blank=True)
|
||||||
|
genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
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 #
|
||||||
|
|
||||||
|
# AudioFeatures {{{ #
|
||||||
|
|
||||||
|
class AudioFeatures(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "AudioFeatures"
|
||||||
|
verbose_name_plural = "AudioFeatures"
|
||||||
|
|
||||||
|
track = models.OneToOneField(Track, on_delete=models.CASCADE, primary_key=True,)
|
||||||
|
acousticness = models.DecimalField(decimal_places=3, max_digits=3)
|
||||||
|
danceability = models.DecimalField(decimal_places=3, max_digits=3)
|
||||||
|
energy = models.DecimalField(decimal_places=3, max_digits=3)
|
||||||
|
instrumentalness = models.DecimalField(decimal_places=3, max_digits=3)
|
||||||
|
loudness = models.DecimalField(decimal_places=3, max_digits=6)
|
||||||
|
speechiness = models.DecimalField(decimal_places=3, max_digits=3)
|
||||||
|
tempo = models.DecimalField(decimal_places=3, max_digits=6)
|
||||||
|
valence = models.DecimalField(decimal_places=3, max_digits=3)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return super(AudioFeatures, self).__str__()
|
||||||
|
|
||||||
|
# }}} AudioFeatures #
|
||||||
12
graphs/urls.py
Normal file
12
graphs/urls.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('artists/<str:user_secret>', artist_data,
|
||||||
|
name='display_artist_graph'),
|
||||||
|
path('genre/<str:user_secret>', display_genre_graph,
|
||||||
|
name='display_genre_graph'),
|
||||||
|
path('audio_features/<str:user_secret>', audio_features,
|
||||||
|
name='display_audio_features'),
|
||||||
|
]
|
||||||
51
graphs/views.py
Normal file
51
graphs/views.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# }}} imports #
|
||||||
|
|
||||||
|
def artist_data(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
|
||||||
|
"""
|
||||||
|
user = User.objects.get(user_secret=user_secret)
|
||||||
|
context = {
|
||||||
|
'user_id': user.user_id,
|
||||||
|
'user_secret': user_secret,
|
||||||
|
}
|
||||||
|
return render(request, "spotifyvis/artist_graph.html", context)
|
||||||
|
|
||||||
|
def display_genre_graph(request, user_secret):
|
||||||
|
user = User.objects.get(user_secret=user_secret)
|
||||||
|
context = {
|
||||||
|
'user_secret': user_secret,
|
||||||
|
}
|
||||||
|
return render(request, "spotifyvis/genre_graph.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def audio_features(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
|
||||||
|
"""
|
||||||
|
user = User.objects.get(user_secret=user_secret)
|
||||||
|
context = {
|
||||||
|
'user_id': user.user_id,
|
||||||
|
'user_secret': user_secret,
|
||||||
|
}
|
||||||
|
return render(request, "spotifyvis/audio_features.html", context)
|
||||||
0
login/__init__.py
Normal file
0
login/__init__.py
Normal file
4
login/apps.py
Normal file
4
login/apps.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class LoginConfig(AppConfig):
|
||||||
|
name = 'login'
|
||||||
11
login/urls.py
Normal file
11
login/urls.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', index, name='index'),
|
||||||
|
path('spotify_login', spotify_login, name='spotify_login'),
|
||||||
|
path('callback', callback, name='callback'),
|
||||||
|
path('user_data', user_data, name='user_data'),
|
||||||
|
path('admin_graphs', admin_graphs, name='admin_graphs'),
|
||||||
|
]
|
||||||
@@ -11,10 +11,7 @@ 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 HttpResponseBadRequest
|
||||||
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 #
|
# }}} imports #
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ def index(request):
|
|||||||
# login {{{ #
|
# login {{{ #
|
||||||
|
|
||||||
# uses Authorization Code flow
|
# uses Authorization Code flow
|
||||||
def login(request):
|
def spotify_login(request):
|
||||||
# use a randomly generated state string to prevent cross-site request forgery attacks
|
# use a randomly generated state string to prevent cross-site request forgery attacks
|
||||||
state_str = generate_random_string(16)
|
state_str = generate_random_string(16)
|
||||||
request.session['state_string'] = state_str
|
request.session['state_string'] = state_str
|
||||||
@@ -117,7 +114,6 @@ def callback(request):
|
|||||||
|
|
||||||
# user_data {{{ #
|
# user_data {{{ #
|
||||||
|
|
||||||
|
|
||||||
def user_data(request):
|
def user_data(request):
|
||||||
|
|
||||||
# get user token {{{ #
|
# get user token {{{ #
|
||||||
@@ -165,7 +161,8 @@ def user_data(request):
|
|||||||
'user_secret': user.user_secret,
|
'user_secret': user.user_secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_library(headers, TRACKS_TO_QUERY, user)
|
# TODO: redirect to API app to parse library or loading page
|
||||||
|
# parse_library(headers, TRACKS_TO_QUERY, user)
|
||||||
return render(request, 'spotifyvis/logged_in.html', context)
|
return render(request, 'spotifyvis/logged_in.html', context)
|
||||||
|
|
||||||
# }}} user_data #
|
# }}} user_data #
|
||||||
@@ -182,107 +179,3 @@ def admin_graphs(request):
|
|||||||
}
|
}
|
||||||
update_track_genres(user_obj)
|
update_track_genres(user_obj)
|
||||||
return render(request, 'spotifyvis/logged_in.html', context)
|
return render(request, 'spotifyvis/logged_in.html', context)
|
||||||
|
|
||||||
|
|
||||||
def artist_data(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
|
|
||||||
"""
|
|
||||||
user = User.objects.get(user_secret=user_secret)
|
|
||||||
context = {
|
|
||||||
'user_id': user.user_id,
|
|
||||||
'user_secret': user_secret,
|
|
||||||
}
|
|
||||||
return render(request, "spotifyvis/artist_graph.html", context)
|
|
||||||
|
|
||||||
# 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(user_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]
|
|
||||||
return JsonResponse(data=processed_artist_counts, safe=False)
|
|
||||||
|
|
||||||
# }}} get_artist_data #
|
|
||||||
|
|
||||||
|
|
||||||
def display_genre_graph(request, user_secret):
|
|
||||||
user = User.objects.get(user_secret=user_secret)
|
|
||||||
context = {
|
|
||||||
'user_secret': user_secret,
|
|
||||||
}
|
|
||||||
return render(request, "spotifyvis/genre_graph.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def audio_features(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
|
|
||||||
"""
|
|
||||||
user = User.objects.get(user_secret=user_secret)
|
|
||||||
context = {
|
|
||||||
'user_id': user.user_id,
|
|
||||||
'user_secret': user_secret,
|
|
||||||
}
|
|
||||||
return render(request, "spotifyvis/audio_features.html", context)
|
|
||||||
|
|
||||||
# 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(user_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(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 #
|
|
||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musicvis.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "spotifyvis.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|||||||
@@ -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),
|
|
||||||
]
|
|
||||||
@@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyvisConfig(AppConfig):
|
|
||||||
name = 'spotifyvis'
|
|
||||||
@@ -37,7 +37,9 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'spotifyvis.apps.SpotifyvisConfig',
|
'login.apps.LoginConfig',
|
||||||
|
'api.apps.ApiConfig',
|
||||||
|
'graphs.apps.GraphsConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -50,7 +52,7 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'musicvis.urls'
|
ROOT_URLCONF = 'spotifyvis.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
@@ -68,7 +70,7 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'musicvis.wsgi.application'
|
WSGI_APPLICATION = 'spotifyvis.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{% 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">
|
|
||||||
<link rel="stylesheet" href="{% static 'spotifyvis/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>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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))
|
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
|
"""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
|
from django.urls import path, include
|
||||||
|
|
||||||
from .views import *
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', index, name='index'),
|
path('admin/', admin.site.urls),
|
||||||
path('login', login, name='login'),
|
path('login/', include('login.urls')),
|
||||||
path('callback', callback, name='callback'),
|
path('api/', include('api.urls')),
|
||||||
path('user_data', user_data, name='user_data'),
|
path('graphs/', include('graphs.urls')),
|
||||||
path('admin_graphs', admin_graphs, name='admin_graphs'),
|
|
||||||
path('api/user_artists/<str:user_secret>', get_artist_data, name='get_artist_data'),
|
|
||||||
path('artists/<str:user_secret>', artist_data, name='display_artist_graph'),
|
|
||||||
path('api/user_genres/<str:user_secret>', get_genre_data, name='get_genre_data'),
|
|
||||||
path('graphs/genre/<str:user_secret>', display_genre_graph,
|
|
||||||
name='display_genre_graph'),
|
|
||||||
path('audio_features/<str:user_secret>', audio_features, name='display_audio_features'),
|
|
||||||
path('api/audio_features/<str:audio_feature>/<str:user_secret>',
|
|
||||||
get_audio_feature_data, name='get_audio_feature_data'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
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()
|
application = get_wsgi_application()
|
||||||
Reference in New Issue
Block a user