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