Chris Shyi
7 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 606 additions and 865 deletions
-
0api/__init__.py
-
0api/admin.py
-
4api/apps.py
-
24api/models.py
-
15api/urls.py
-
193api/utils.py
-
186api/views.py
-
0graphs/__init__.py
-
4graphs/apps.py
-
0graphs/static/graphs/scripts/artist_graph.js
-
0graphs/static/graphs/scripts/audio_feat_graph.js
-
0graphs/static/graphs/scripts/genre_graph.js
-
5graphs/templates/graphs/artist_graph.html
-
3graphs/templates/graphs/features_graphs.html
-
6graphs/templates/graphs/genre_graph.html
-
8graphs/templates/graphs/logged_in.html
-
13graphs/urls.py
-
8graphs/utils.py
-
42graphs/views.py
-
0login/__init__.py
-
4login/apps.py
-
22login/models.py
-
9login/templates/login/index.html
-
8login/templates/login/scan.html
-
12login/urls.py
-
10login/utils.py
-
142login/views.py
-
2manage.py
-
22musicvis/urls.py
-
8recreate-db.txt
-
13reset_db.sh
-
250sample-track-obj.py
-
5spotifyvis/apps.py
-
11spotifyvis/settings.py
-
42spotifyvis/static/spotifyvis/scripts/index.js
-
67spotifyvis/tests.py
-
35spotifyvis/urls.py
-
288spotifyvis/views.py
-
2spotifyvis/wsgi.py
-
0static/css/dark_bg.css
@ -0,0 +1,4 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
class ApiConfig(AppConfig): |
||||
|
name = 'api' |
@ -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'), |
||||
|
] |
@ -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,0 +1,4 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
class GraphsConfig(AppConfig): |
||||
|
name = 'graphs' |
@ -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'), |
||||
|
] |
@ -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, } |
@ -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,0 +1,4 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
class LoginConfig(AppConfig): |
||||
|
name = 'login' |
@ -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 |
@ -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'), |
||||
|
] |
@ -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, } |
@ -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 # |
@ -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,8 +0,0 @@ |
|||||
# https://stackoverflow.com/a/34576062/8811872 |
|
||||
|
|
||||
sudo su postgres |
|
||||
psql |
|
||||
drop database spotifyvis; |
|
||||
create database spotifyvis with owner django; |
|
||||
\q |
|
||||
exit |
|
@ -1,14 +1,15 @@ |
|||||
# check if in virtual environment |
# check if in virtual environment |
||||
# https://stackoverflow.com/questions/15454174/how-can-a-shell-function-know-if-it-is-running-within-a-virtualenv/15454916 |
# 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 |
|
||||
|
# 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")') |
||||
|
|
||||
# echo $INVENV |
|
||||
# if $INVENV is 1, then in virtualenv |
# if $INVENV is 1, then in virtualenv |
||||
|
|
||||
if [ $INVENV -eq 1 ]; then |
|
||||
rm spotifyvis/migrations/00* |
|
||||
|
# echo $INVENV |
||||
|
# if [ $INVENV -eq 1 ]; then |
||||
|
rm login/migrations/0* api/migrations/0* |
||||
sudo -u postgres psql -f reset_db.sql |
sudo -u postgres psql -f reset_db.sql |
||||
python manage.py makemigrations |
python manage.py makemigrations |
||||
python manage.py migrate |
python manage.py migrate |
||||
fi |
|
||||
|
python manage.py runserver |
||||
|
# fi |
@ -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' |
|
@ -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,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 @@ |
|||||
from django.urls import path, include |
|
||||
|
"""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 = [ |
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('api/user_artists/<str:user_secret>', get_artist_data, name='get_artist_data'), |
|
||||
path('graphs/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('graphs/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'), |
|
||||
|
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")), |
||||
] |
] |
@ -1,288 +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 # |
|
||||
|
|
||||
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 # |
|
||||
|
|
||||
# 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 |
|
||||
} |
|
||||
|
|
||||
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) |
|
||||
|
|
||||
# }}} 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 {{{ # |
|
||||
|
|
||||
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(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 = { |
|
||||
'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 # |
|
||||
|
|
||||
def admin_graphs(request): |
|
||||
"""TODO |
|
||||
""" |
|
||||
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, |
|
||||
} |
|
||||
update_track_genres(user_obj) |
|
||||
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 # |
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue