From e614b373df0b6ed864a149d5fbe2892bd361c7a0 Mon Sep 17 00:00:00 2001 From: Chris Shyi Date: Fri, 22 Jun 2018 16:28:47 -0400 Subject: [PATCH 1/9] Fix program crash when AudioFeatures is missing Fixes #32. AudioFeatures object query is now wrapped in a try/except block to account for the scenario where the object doesn't exist. --- spotifyvis/utils.py | 2 +- spotifyvis/views.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spotifyvis/utils.py b/spotifyvis/utils.py index b7a95c7..7898141 100644 --- a/spotifyvis/utils.py +++ b/spotifyvis/utils.py @@ -127,7 +127,7 @@ def save_audio_features(headers, track_id, track): response = requests.get("https://api.spotify.com/v1/audio-features/{}".format(track_id), headers = headers).json() if 'error' in response: - return {} + return # Data that we don't need useless_keys = [ diff --git a/spotifyvis/views.py b/spotifyvis/views.py index 8b9e3b8..a21b6d3 100644 --- a/spotifyvis/views.py +++ b/spotifyvis/views.py @@ -203,8 +203,11 @@ def get_audio_feature_data(request, audio_feature, client_secret): 'data_points': [], } for track in user_tracks: - audio_feature_obj = AudioFeatures.objects.get(track=track) - response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature)) + 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 # From de93bd7b0ad4ca486e3c355dd22ff13e70651712 Mon Sep 17 00:00:00 2001 From: Kevin Mok Date: Sun, 24 Jun 2018 02:30:19 -0400 Subject: [PATCH 2/9] Store counts for genres in Genre model Have not assigned most common genre to Track models yet (currently blank). --- recreate-db.txt | 8 ++++++++ requirements.txt | 2 +- spotifyvis/models.py | 17 +++++++++++++++-- spotifyvis/utils.py | 35 +++++++++++++++++++++++++++++------ 4 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 recreate-db.txt diff --git a/recreate-db.txt b/recreate-db.txt new file mode 100644 index 0000000..5c1e574 --- /dev/null +++ b/recreate-db.txt @@ -0,0 +1,8 @@ +# https://stackoverflow.com/a/34576062/8811872 + +sudo su postgres +psql +drop database spotifyvis; +create database spotifyvis with owner django; +\q +exit diff --git a/requirements.txt b/requirements.txt index 7eba139..59c9dc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ idna==2.6 isort==4.3.4 lazy-object-proxy==1.3.1 mccabe==0.6.1 -psycopg2==2.7.4 +psycopg2-binary==2.7.4 pylint==1.8.4 pytz==2018.4 requests==2.18.4 diff --git a/spotifyvis/models.py b/spotifyvis/models.py index 99f93d8..398647e 100644 --- a/spotifyvis/models.py +++ b/spotifyvis/models.py @@ -35,6 +35,18 @@ class User(models.Model): # }}} User # +class Genre(models.Model): + + class Meta: + verbose_name = "Genre" + verbose_name_plural = "Genres" + + name = models.CharField(primary_key=True, max_length=50) + num_songs = models.PositiveIntegerField() + + def __str__(self): + return self.name + # Track {{{ # class Track(models.Model): @@ -51,7 +63,9 @@ class Track(models.Model): runtime = models.PositiveSmallIntegerField() name = models.CharField(max_length=200) users = models.ManyToManyField(User, blank=True) - genre = models.CharField(max_length=30) + # genre = models.CharField(max_length=30) + genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True, + null=True) def __str__(self): return self.name @@ -60,7 +74,6 @@ class Track(models.Model): # AudioFeatures {{{ # - class AudioFeatures(models.Model): class Meta: diff --git a/spotifyvis/utils.py b/spotifyvis/utils.py index 7898141..e4dc4ec 100644 --- a/spotifyvis/utils.py +++ b/spotifyvis/utils.py @@ -3,8 +3,8 @@ import requests import math import pprint -from .models import Artist, User, Track, AudioFeatures -from django.db.models import Count, Q +from .models import * +from django.db.models import Count, Q, F from django.http import JsonResponse from django.core import serializers import json @@ -13,7 +13,6 @@ import json # parse_library {{{ # - def parse_library(headers, tracks, user): """Scans user's library for certain number of tracks to update library_stats with. @@ -48,6 +47,8 @@ def parse_library(headers, tracks, user): artist_id=artist_dict['id'], name=artist_dict['name'], ) + if artist_created: + tally_artist_genres(headers, artist_dict['id']) # update_artist_genre(headers, artist_obj) # get_or_create() returns a tuple (obj, created) @@ -59,8 +60,8 @@ def parse_library(headers, tracks, user): track_artists, top_genre, user) # if a new track is not created, the associated audio feature does not need to be created again - if track_created: - save_audio_features(headers, track_dict['track']['id'], track_obj) + # if track_created: + save_audio_features(headers, track_dict['track']['id'], track_obj) """ TODO: Put this logic in another function # Audio analysis could be empty if not present in Spotify database @@ -100,7 +101,7 @@ def save_track_obj(track_dict, artists, top_genre, user): popularity=int(track_dict['popularity']), runtime=int(float(track_dict['duration_ms']) / 1000), name=track_dict['name'], - genre=top_genre, + # genre=top_genre, ) # have to add artists and user after saving object since track needs to @@ -126,6 +127,8 @@ def save_audio_features(headers, track_id, track): """ response = requests.get("https://api.spotify.com/v1/audio-features/{}".format(track_id), headers = headers).json() + if track_id is '5S1IUPueD0xE0vj4zU3nSf': + pprint.pprint(response) if 'error' in response: return @@ -330,6 +333,7 @@ def get_top_genre(headers, top_artist_id): """ artist_response = requests.get('https://api.spotify.com/v1/artists/' + top_artist_id, headers=headers).json() + # pprint.pprint(artist_response) if len(artist_response['genres']) > 0: return artist_response['genres'][0] else: @@ -337,6 +341,25 @@ def get_top_genre(headers, top_artist_id): # }}} # +def tally_artist_genres(headers, artist_id): + """Tallies up genres for artist for the respective Genre models. Should be + called when new Artist object is created. + + :headers: For making the API call. + :artist_id: Artist ID for which to tally up genres for. + + :returns: None + + """ + artist_response = requests.get('https://api.spotify.com/v1/artists/' + + artist_id, headers=headers).json() + for genre in artist_response['genres']: + genre_obj, created = Genre.objects.get_or_create(name=genre, + defaults={'num_songs':1}) + if not created: + genre_obj.num_songs = F('num_songs') +1 + genre_obj.save() + # process_library_stats {{{ # def process_library_stats(library_stats): From 4e1a6df89e5fdd33e7feeb63541fde6be20cfb41 Mon Sep 17 00:00:00 2001 From: Kevin Mok Date: Sun, 24 Jun 2018 07:05:54 -0400 Subject: [PATCH 3/9] Store genres in artists as m2m field (#34) --- spotifyvis/models.py | 29 +++++++++++++++++------------ spotifyvis/utils.py | 14 ++++++++------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/spotifyvis/models.py b/spotifyvis/models.py index 398647e..b6563ed 100644 --- a/spotifyvis/models.py +++ b/spotifyvis/models.py @@ -3,6 +3,22 @@ from django.db import models # id's are 22 in length in examples but set to 30 for buffer MAX_ID = 30 +# Genre {{{ # + +class Genre(models.Model): + + class Meta: + verbose_name = "Genre" + verbose_name_plural = "Genres" + + name = models.CharField(primary_key=True, max_length=50) + num_songs = models.PositiveIntegerField() + + def __str__(self): + return self.name + +# }}} Genre # + # Artist {{{ # @@ -14,6 +30,7 @@ class Artist(models.Model): artist_id = models.CharField(primary_key=True, max_length=MAX_ID) # unique since only storing one genre per artist right now name = models.CharField(unique=True, max_length=50) + genres = models.ManyToManyField(Genre, blank=True) def __str__(self): return self.name @@ -35,18 +52,6 @@ class User(models.Model): # }}} User # -class Genre(models.Model): - - class Meta: - verbose_name = "Genre" - verbose_name_plural = "Genres" - - name = models.CharField(primary_key=True, max_length=50) - num_songs = models.PositiveIntegerField() - - def __str__(self): - return self.name - # Track {{{ # class Track(models.Model): diff --git a/spotifyvis/utils.py b/spotifyvis/utils.py index e4dc4ec..7c4c088 100644 --- a/spotifyvis/utils.py +++ b/spotifyvis/utils.py @@ -48,7 +48,7 @@ def parse_library(headers, tracks, user): name=artist_dict['name'], ) if artist_created: - tally_artist_genres(headers, artist_dict['id']) + add_artist_genres(headers, artist_obj) # update_artist_genre(headers, artist_obj) # get_or_create() returns a tuple (obj, created) @@ -341,24 +341,26 @@ def get_top_genre(headers, top_artist_id): # }}} # -def tally_artist_genres(headers, artist_id): - """Tallies up genres for artist for the respective Genre models. Should be - called when new Artist object is created. +def add_artist_genres(headers, artist_obj): + """Adds genres to artist_obj and increases the count the respective Genre + object. Should be called when a new Artist object is created. :headers: For making the API call. - :artist_id: Artist ID for which to tally up genres for. + :artist_obj: Artist object for which to add/tally up genres for. :returns: None """ artist_response = requests.get('https://api.spotify.com/v1/artists/' + - artist_id, headers=headers).json() + artist_obj.artist_id, headers=headers).json() for genre in artist_response['genres']: genre_obj, created = Genre.objects.get_or_create(name=genre, defaults={'num_songs':1}) if not created: genre_obj.num_songs = F('num_songs') +1 genre_obj.save() + artist_obj.genres.add(genre_obj) + artist_obj.save() # process_library_stats {{{ # From 709ed9b491f985ca59422f8d54b3786c56a98e8e Mon Sep 17 00:00:00 2001 From: Kevin Mok Date: Mon, 25 Jun 2018 03:14:39 -0400 Subject: [PATCH 4/9] Added most common genre for tracks with 1 artist Still have to find shared genres for songs with multiple artists (see #34). --- spotifyvis/utils.py | 22 ++++++++++++++++++++++ spotifyvis/views.py | 6 ++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/spotifyvis/utils.py b/spotifyvis/utils.py index 7c4c088..35160a8 100644 --- a/spotifyvis/utils.py +++ b/spotifyvis/utils.py @@ -76,9 +76,27 @@ def parse_library(headers, tracks, user): # calculates num_songs with offset + songs retrieved offset += limit # pprint.pprint(library_stats) + update_track_genres(user) # }}} parse_library # +def update_track_genres(user): + """Updates user's tracks with the most common genre associated with the + songs' artist(s). + + :user: User object who's tracks are being updated. + + :returns: None + + """ + user_tracks = Track.objects.filter(users__exact=user) + for track in user_tracks: + track_artists = list(track.artists.all()) + if len(track_artists) == 1: + track.genre = track_artists[0].genres.all().order_by('-num_songs').first() + track.save() + # print(track_artists, track.genre) + # save_track_obj {{{ # def save_track_obj(track_dict, artists, top_genre, user): @@ -341,6 +359,8 @@ def get_top_genre(headers, top_artist_id): # }}} # +# add_artist_genres {{{ # + def add_artist_genres(headers, artist_obj): """Adds genres to artist_obj and increases the count the respective Genre object. Should be called when a new Artist object is created. @@ -362,6 +382,8 @@ def add_artist_genres(headers, artist_obj): artist_obj.genres.add(genre_obj) artist_obj.save() +# }}} add_artist_genres # + # process_library_stats {{{ # def process_library_stats(library_stats): diff --git a/spotifyvis/views.py b/spotifyvis/views.py index a21b6d3..1cf89f5 100644 --- a/spotifyvis/views.py +++ b/spotifyvis/views.py @@ -13,7 +13,7 @@ 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, process_library_stats, get_artists_in_genre +from .utils import parse_library, process_library_stats, get_artists_in_genre, update_track_genres from .models import User, Track, AudioFeatures, Artist # }}} imports # @@ -165,10 +165,12 @@ def test_db(request): """TODO """ user_id = "polarbier" + user_obj = User.objects.get(user_id=user_id) # user_id = "35kxo00qqo9pd1comj6ylxjq7" context = { - 'user_secret': User.objects.get(user_id=user_id).user_secret, + 'user_secret': user_obj.user_secret, } + update_track_genres(user_obj) return render(request, 'spotifyvis/test_db.html', context) # }}} test_db # From df9547293fa1f41fcd052f1efa61280ffb960b48 Mon Sep 17 00:00:00 2001 From: Kevin Mok Date: Mon, 25 Jun 2018 04:20:55 -0400 Subject: [PATCH 5/9] Changed all existing pages to use a dark bg --- spotifyvis/static/spotifyvis/css/dark_bg.css | 8 ++++++++ spotifyvis/templates/spotifyvis/index.html | 6 +++--- spotifyvis/templates/spotifyvis/test_db.html | 2 ++ spotifyvis/templates/spotifyvis/user_data.html | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 spotifyvis/static/spotifyvis/css/dark_bg.css diff --git a/spotifyvis/static/spotifyvis/css/dark_bg.css b/spotifyvis/static/spotifyvis/css/dark_bg.css new file mode 100644 index 0000000..a472959 --- /dev/null +++ b/spotifyvis/static/spotifyvis/css/dark_bg.css @@ -0,0 +1,8 @@ +body { +background-color: #1e1e1e; +} + +h1,p { +color: grey; +} + diff --git a/spotifyvis/templates/spotifyvis/index.html b/spotifyvis/templates/spotifyvis/index.html index 5964b34..28a9301 100644 --- a/spotifyvis/templates/spotifyvis/index.html +++ b/spotifyvis/templates/spotifyvis/index.html @@ -4,6 +4,7 @@ User Login +