diff --git a/.gitignore b/.gitignore index c492bbd..d7ce0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +*.log db.sqlite3 *.bak .idea/ @@ -9,3 +10,4 @@ api-keys.sh Pipfile *.txt scrap.py +media/history/* diff --git a/api/management/commands/update-history.py b/api/management/commands/update-history.py new file mode 100644 index 0000000..0e48296 --- /dev/null +++ b/api/management/commands/update-history.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand, CommandError +from api.utils import parse_history +from login.models import User + +class Command(BaseCommand): + help = 'Update history for users who requested it' + + def handle(self, *args, **options): + user_id = "polarbier" + parse_history(User.objects.get(id=user_id).secret) diff --git a/api/models.py b/api/models.py index 41daa01..5d6b4cf 100644 --- a/api/models.py +++ b/api/models.py @@ -45,9 +45,8 @@ class Track(models.Model): verbose_name_plural = "Tracks" id = models.CharField(primary_key=True, max_length=MAX_ID) - # artist = models.ForeignKey(Artist, on_delete=models.CASCADE) artists = models.ManyToManyField(Artist, blank=True) - year = models.PositiveSmallIntegerField() + year = models.PositiveSmallIntegerField(null=True) popularity = models.PositiveSmallIntegerField() runtime = models.PositiveSmallIntegerField() name = models.CharField(max_length=200) @@ -86,3 +85,30 @@ class AudioFeatures(models.Model): return super(AudioFeatures, self).__str__() # }}} AudioFeatures # + +# History {{{ # + +class History(models.Model): + + class Meta: + verbose_name = "History" + verbose_name_plural = "History" + unique_together = (("user", "timestamp"),) + + id = models.AutoField(primary_key=True) + user = models.ForeignKey(User, on_delete=models.CASCADE) + timestamp = models.DateTimeField() + track = models.ForeignKey(Track, on_delete=models.CASCADE) + + def __str__(self): + return " - ".join((str(self.user), str(self.timestamp), str(self.track))) + + def get_artists(self): + artist_names = [artist.name for artist in self.track.artists.all()] + return ', '.join(artist_names) + + def get_iso_timestamp(self): + return self.timestamp.isoformat() + +# }}} # + diff --git a/api/urls.py b/api/urls.py index 60126f6..4769690 100644 --- a/api/urls.py +++ b/api/urls.py @@ -4,12 +4,16 @@ from .views import * app_name = 'api' urlpatterns = [ - path('scan/', parse_library, - name='scan'), + path('scan/library/', parse_library, + name='scan_library'), + path('scan/history/', parse_history_request, + name='scan_history'), path('user_artists/', get_artist_data, name='get_artist_data'), path('user_genres/', get_genre_data, name='get_genre_data'), path('audio_features//', get_audio_feature_data, name='get_audio_feature_data'), + path('import/history/', import_history, name='import_history'), ] + diff --git a/api/utils.py b/api/utils.py index ef0c99f..6ac7995 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,21 +1,26 @@ # imports {{{ # import requests import math -import pprint import os import json -from django.db.models import Count, Q, F +from django.db.models import Count, F, Max from django.http import JsonResponse from django.core import serializers from django.utils import timezone from .models import * +from . import views from login.models import User +from pprint import pprint +from dateutil.parser import parse +from datetime import datetime + +HISTORY_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played' # }}} imports # -console_logging = True -# console_logging = False +# console_logging = True +console_logging = False artists_genre_processed = 0 features_processed = 0 @@ -74,19 +79,32 @@ def save_track_obj(track_dict, artists, user_obj): if len(track_query) != 0: return track_query[0], False else: - new_track = Track.objects.create( - id=track_dict['id'], - year=track_dict['album']['release_date'].split('-')[0], - popularity=int(track_dict['popularity']), - runtime=int(float(track_dict['duration_ms']) / 1000), - name=track_dict['name'], - ) + # check if track is simple or full, simple Track object won't have year + # if 'album' in track_dict: + try: + new_track = Track.objects.create( + id=track_dict['id'], + year=track_dict['album']['release_date'].split('-')[0], + popularity=int(track_dict['popularity']), + runtime=int(float(track_dict['duration_ms']) / 1000), + name=track_dict['name'], + ) + # else: + except KeyError: + new_track = Track.objects.create( + id=track_dict['id'], + popularity=int(track_dict['popularity']), + runtime=int(float(track_dict['duration_ms']) / 1000), + name=track_dict['name'], + ) # have to add artists and user_obj after saving object since track needs to # have ID before filling in m2m field for artist in artists: new_track.artists.add(artist) - new_track.users.add(user_obj) + # print(new_track.name, artist.name) + if user_obj != None: + new_track.users.add(user_obj) new_track.save() return new_track, True @@ -169,8 +187,8 @@ def add_artist_genres(headers, artist_objs): artist_ids = str.join(",", [artist_obj.id for artist_obj in artist_objs]) params = {'ids': artist_ids} artists_response = requests.get('https://api.spotify.com/v1/artists/', - headers=headers, - params=params, + headers=headers, + params={'ids': artist_ids}, ).json()['artists'] for i in range(len(artist_objs)): if len(artists_response[i]['genres']) == 0: @@ -178,6 +196,7 @@ def add_artist_genres(headers, artist_objs): else: for genre in artists_response[i]['genres']: process_artist_genre(genre, artist_objs[i]) + # print(artist_objs[i].name, genre) if console_logging: global artists_genre_processed @@ -221,6 +240,35 @@ def get_artists_in_genre(user, genre, max_songs): # }}} get_artists_in_genre # +# save_track_artists {{{ # + +def save_track_artists(track_dict, artist_genre_queue, user_headers): + """ Update artist info before creating Track so that Track object can + reference Artist object. + + :track_dict: response from Spotify API for track + :returns: list of Artist objects in Track + + """ + track_artists = [] + for artist_dict in track_dict['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) == views.ARTIST_LIMIT: + add_artist_genres(user_headers, artist_genre_queue) + artist_genre_queue[:] = [] + track_artists.append(artist_obj) + + return track_artists + +# }}} save_track_artists # + +# get_user_header {{{ # + def get_user_header(user_obj): """Returns the authorization string needed to make an API call. @@ -246,3 +294,105 @@ def get_user_header(user_obj): user_obj.save() return {'Authorization': "Bearer " + user_obj.access_token} + +# }}} get_user_header # + +# save_history_obj {{{ # + +def save_history_obj (user, timestamp, track): + """Return (get/create) a History object with the specified parameters. Can't + use built-in get_or_create since don't know auto PK. + + :user: User object History should be associated with + :timestamp: time at which song was listened to + :track: Track object for song + :returns: History object + + """ + history_query = History.objects.filter(user__exact=user, + timestamp__exact=timestamp) + if len(history_query) == 0: + history_obj = History.objects.create(user=user, timestamp=timestamp, + track=track) + else: + history_obj = history_query[0] + + return history_obj + +# }}} save_history_obj # + +# get_next_history_row {{{ # + +def get_next_history_row(csv_reader, headers, prev_info): + """Return formatted information from next row in history CSV file. + + :csv_reader: TODO + :headers: + :prev_info: history_obj_info of last row in case no more rows + :returns: (boolean of if last row, dict with information of next row) + + """ + try: + row = next(csv_reader) + # if Track.objects.filter(id__exact=row[1]).exists(): + history_obj_info = {} + for i in range(len(headers)): + history_obj_info[headers[i]] = row[i] + return False, history_obj_info + except StopIteration: + return True, prev_info + +# }}} get_next_history_row # + +# parse_history {{{ # + +def parse_history(user_secret): + """Scans user's listening history and stores the information in a + database. + + :user_secret: secret for User object who's library is being scanned. + :returns: None + """ + + user_obj = User.objects.get(secret=user_secret) + payload = {'limit': str(views.USER_TRACKS_LIMIT)} + last_time_played = History.objects.filter(user=user_obj).aggregate(Max('timestamp'))['timestamp__max'] + if last_time_played is not None: + payload['after'] = last_time_played.isoformat() + artist_genre_queue = [] + user_headers = get_user_header(user_obj) + history_response = requests.get(HISTORY_ENDPOINT, + headers=user_headers, + params=payload).json()['items'] + # pprint(history_response) + + tracks_processed = 0 + + for track_dict in history_response: + # don't associate history track with User, not necessarily in their + # library + # track_obj, track_created = save_track_obj(track_dict['track'], + # track_artists, None) + track_artists = save_track_artists(track_dict['track'], artist_genre_queue, + user_headers) + track_obj, track_created = save_track_obj(track_dict['track'], + track_artists, None) + history_obj = save_history_obj(user_obj, parse(track_dict['played_at']), + track_obj) + tracks_processed += 1 + + if console_logging: + print("Added history track #{}: {}".format( + tracks_processed, history_obj,)) + + if len(artist_genre_queue) > 0: + add_artist_genres(user_headers, artist_genre_queue) + + # TODO: update track genres from History relation + # update_track_genres(user_obj) + + print("Scanned {} history tracks for user {} at {}.".format( + tracks_processed, user_obj.id, datetime.now())) + +# }}} get_history # + diff --git a/api/views.py b/api/views.py index 2f8e96c..c9dd565 100644 --- a/api/views.py +++ b/api/views.py @@ -5,27 +5,39 @@ import random import requests import urllib import secrets -import pprint import string +import csv from django.shortcuts import render, redirect from django.http import JsonResponse -from django.db.models import Count, Q +from django.db.models import Count, Q, Max +from django.core.files import File from .utils import * from .models import * from login.models import User from login.utils import get_user_context +from dateutil.parser import parse +from pprint import pprint +from login.models import HistoryUpload # }}} imports # +# constants {{{ # + USER_TRACKS_LIMIT = 50 +TRACKS_LIMIT = 50 +HISTORY_LIMIT = 50 ARTIST_LIMIT = 50 FEATURES_LIMIT = 100 # ARTIST_LIMIT = 25 # FEATURES_LIMIT = 25 TRACKS_TO_QUERY = 100 +TRACKS_ENDPOINT = 'https://api.spotify.com/v1/tracks' console_logging = True +# console_logging = False + +# }}} constants # # parse_library {{{ # @@ -47,7 +59,8 @@ def parse_library(request, user_secret): # 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: + 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, @@ -57,25 +70,8 @@ def parse_library(request, user_secret): 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_artists = save_track_artists(track_dict['track'], artist_genre_queue, + user_headers) track_obj, track_created = save_track_obj(track_dict['track'], track_artists, user_obj) @@ -119,6 +115,21 @@ def parse_library(request, user_secret): # }}} parse_library # +# parse_history_request {{{ # + +def parse_history_request(request, user_secret): + """Request function to call parse_history. Scans user's listening history + and stores the information in a database. + + :user_secret: secret for User object who's library is being scanned. + :returns: redirects user to logged in page + """ + parse_history(user_secret) + return render(request, 'graphs/logged_in.html', + get_user_context(User.objects.get(secret=user_secret))) + +# }}} get_history # + # get_artist_data {{{ # def get_artist_data(request, user_secret): @@ -134,7 +145,7 @@ def get_artist_data(request, user_secret): 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) + pprint(processed_artist_counts) return JsonResponse(data=processed_artist_counts, safe=False) # }}} get_artist_data # @@ -181,7 +192,87 @@ def get_genre_data(request, user_secret): genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'], genre_dict['num_songs']) print("*** Genre Breakdown ***") - pprint.pprint(list(genre_counts)) + pprint(list(genre_counts)) return JsonResponse(data=list(genre_counts), safe=False) # }}} get_genre_data # + +# import_history {{{ # + +def import_history(request, upload_id): + """Import history for the user from the file they uploaded. + + :upload_id: ID (PK) of the HistoryUpload entry + :returns: None + """ + + # setup {{{ # + + headers = ['timestamp', 'track_id'] + upload_obj = HistoryUpload.objects.get(id=upload_id) + user_headers = get_user_header(upload_obj.user) + + with upload_obj.document.open('r') as f: + csv_reader = csv.reader(f, delimiter=',') + rows_read = 0 + history_obj_info_lst = [] + artist_genre_queue = [] + + # skip header row + last_row, history_obj_info = get_next_history_row(csv_reader, headers, + {}) + while not last_row: + last_row, history_obj_info = get_next_history_row(csv_reader, + headers, history_obj_info) + + # }}} setup # + + history_obj_info_lst.append(history_obj_info) + # PU: refactor saving History object right away if Track obj already + # exists + # PU: refactor below? + rows_read += 1 + if (rows_read % TRACKS_LIMIT == 0) or last_row: + # get tracks_response {{{ # + + track_ids_lst = [info['track_id'] for info in history_obj_info_lst] + # print(len(track_ids_lst)) + track_ids = ','.join(track_ids_lst) + payload = {'ids': track_ids} + tracks_response = requests.get(TRACKS_ENDPOINT, + headers=user_headers, + params=payload).json()['tracks'] + responses_processed = 0 + + # }}} get tracks_response # + + for track_dict in tracks_response: + # don't associate history track with User, not necessarily in their + # library + track_artists = save_track_artists(track_dict, artist_genre_queue, + user_headers) + track_obj, track_created = save_track_obj(track_dict, + track_artists, None) + + timestamp = \ + parse(history_obj_info_lst[responses_processed]['timestamp']) + history_obj = save_history_obj(upload_obj.user, timestamp, + track_obj) + + if console_logging: + print("Processed row #{}: {}".format( + (rows_read - TRACKS_LIMIT) + responses_processed, history_obj,)) + responses_processed += 1 + + history_obj_info_lst = [] + + if len(artist_genre_queue) > 0: + add_artist_genres(user_headers, artist_genre_queue) + + # TODO: update track genres from History relation + # update_track_genres(user_obj) + + return redirect('graphs:display_history_table') + +# }}} get_history # + diff --git a/graphs/templates/graphs/genre_graph.html b/graphs/templates/graphs/genre_graph.html index bf7324e..7d35ee8 100644 --- a/graphs/templates/graphs/genre_graph.html +++ b/graphs/templates/graphs/genre_graph.html @@ -21,10 +21,18 @@ + {% load static %} - + + + + diff --git a/graphs/templates/graphs/logged_in.html b/graphs/templates/graphs/logged_in.html index d553f92..f34655d 100644 --- a/graphs/templates/graphs/logged_in.html +++ b/graphs/templates/graphs/logged_in.html @@ -16,5 +16,8 @@ Artists + + History + diff --git a/graphs/templates/graphs/user_history.html b/graphs/templates/graphs/user_history.html new file mode 100644 index 0000000..a7cc84b --- /dev/null +++ b/graphs/templates/graphs/user_history.html @@ -0,0 +1,17 @@ + +{% load static %} +{% load render_table export_url from django_tables2 %} + + + + User History + + + + +

{{ user_id }}'s Listening History

+

Found {{ total_history }} songs.

+ Export + {% render_table user_history_table %} + + diff --git a/graphs/urls.py b/graphs/urls.py index e1bf0b2..527822b 100644 --- a/graphs/urls.py +++ b/graphs/urls.py @@ -10,4 +10,5 @@ urlpatterns = [ name='display_genre_graph'), path('audio_features/', display_features_graphs, name='display_audio_features'), + path('history/', HistoryList.as_view(), name='display_history_table'), ] diff --git a/graphs/utils.py b/graphs/utils.py index d2cf67c..14c037e 100644 --- a/graphs/utils.py +++ b/graphs/utils.py @@ -1,3 +1,19 @@ +import django_tables2 as tables + +from pprint import pprint +from login.models import User +from api.models import History + +class HistoryTable(tables.Table): + class Meta: + model = History + template_name = 'django_tables2/bootstrap.html' + + iso_timestamp = tables.Column(accessor='get_iso_timestamp', orderable=False) + track_id = tables.Column(accessor='track.id', orderable=False) + track_name = tables.Column(accessor='track.name', orderable=False) + artists = tables.Column(accessor='get_artists', orderable=False) + def get_secret_context(user_secret): """Return user_secret in context for graph pages. diff --git a/graphs/views.py b/graphs/views.py index 25c8177..4b5278a 100644 --- a/graphs/views.py +++ b/graphs/views.py @@ -6,12 +6,17 @@ import requests import os import urllib import secrets -import pprint import string -from datetime import datetime +from pprint import pprint +from datetime import datetime +from time import strftime from django.shortcuts import render, redirect from .utils import * +from django_tables2 import RequestConfig, SingleTableView +from django_tables2.export.views import ExportMixin +from django_tables2.export import TableExport +from api.models import History # }}} imports # @@ -40,3 +45,48 @@ def display_features_graphs(request, user_secret): """ return render(request, "graphs/features_graphs.html", get_secret_context(user_secret)) + +# HistoryList {{{ # + +class HistoryList(ExportMixin, SingleTableView): + """Create table with list of song history.""" + model = History + table_class = HistoryTable + context_table_name = 'user_history_table' + template_name = 'graphs/user_history.html' + + # overridden methods {{{ # + + def get_table_kwargs(self): + return { 'exclude': ('id', 'user', 'track', 'track_id', 'iso_timestamp', ) } + + def get_table_data(self): + return History.objects.filter(user__exact=self.request.session['user_id']).order_by('-timestamp') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['user_id'] = self.request.session['user_id'] + context['total_history'] = self.get_table_data().count() + return context + + def get_export_filename(self, export_format): + user_id = self.request.session['user_id'] + # timestamp = strftime("%m%d%Y-%H%M") + # return "{}.{}".format("-".join((user_id, timestamp)), export_format) + return "{}.{}".format(user_id, export_format) + + def create_export(self, export_format): + export_exclude = ('id', 'user', 'track', 'track_name', 'artists', + 'timestamp', ) + exporter = TableExport( + export_format=export_format, + table=self.get_table(exclude=export_exclude), + exclude_columns=self.exclude_columns, + ) + + return exporter.response(filename=self.get_export_filename(export_format)) + + # }}} overridden methods # + +# }}} HistoryList # + diff --git a/login/forms.py b/login/forms.py new file mode 100644 index 0000000..3863e50 --- /dev/null +++ b/login/forms.py @@ -0,0 +1,8 @@ +from django import forms +from .models import HistoryUpload + +class HistoryUploadForm(forms.ModelForm): + class Meta: + model = HistoryUpload + fields = ('user', 'document', ) + widgets = { 'user': forms.HiddenInput() } diff --git a/login/models.py b/login/models.py index aa04baf..2679b61 100644 --- a/login/models.py +++ b/login/models.py @@ -20,3 +20,8 @@ class User(models.Model): def __str__(self): return self.id + +class HistoryUpload(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + document = models.FileField(upload_to='history/') + uploaded_at = models.DateTimeField(auto_now_add=True) diff --git a/login/templates/login/index.html b/login/templates/login/index.html index 3a41ec5..eb428de 100644 --- a/login/templates/login/index.html +++ b/login/templates/login/index.html @@ -20,7 +20,7 @@

spotify-lib-vis

- Login + Login Admin Graphs
diff --git a/login/templates/login/scan.html b/login/templates/login/scan.html index 183742c..7ea75a4 100644 --- a/login/templates/login/scan.html +++ b/login/templates/login/scan.html @@ -18,8 +18,16 @@

You are using an outdated browser. Please upgrade your browser to improve your experience.

Logged in as {{ user_id }}

- + Scan Library + + Scan History + +
+ {% csrf_token %} + {{ form.as_p }} + +
diff --git a/login/urls.py b/login/urls.py index 75c29e8..120cf7f 100644 --- a/login/urls.py +++ b/login/urls.py @@ -9,4 +9,5 @@ urlpatterns = [ path('callback', callback, name='callback'), # path('user/', user_home, name='user_home'), path('admin_graphs', admin_graphs, name='admin_graphs'), + path('upload_history', upload_history, name='upload_history'), ] diff --git a/login/utils.py b/login/utils.py index 695bfae..d67ede8 100644 --- a/login/utils.py +++ b/login/utils.py @@ -1,4 +1,10 @@ +import string +import random +import requests +import secrets + from .models import User +from .forms import HistoryUploadForm def get_user_context(user_obj): """Get context for rendering with User's ID and secret. @@ -8,3 +14,70 @@ def get_user_context(user_obj): """ return { 'user_id': user_obj.id, 'user_secret': user_obj.secret, } + +# 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 # + +# create_user {{{ # + + +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 + +# }}} create_user # + +def get_scan_context(request): + """Get context for rendering scan page. + + :request: + :returns: Context with upload form and user info. + + """ + context = { 'user_id': request.session['user_id'], + 'user_secret': request.session['user_secret'], } + # set hidden user field to current user + context['form'] = HistoryUploadForm(initial= + { 'user': User.objects.get(id=request.session['user_id']) }) + return context diff --git a/login/views.py b/login/views.py index 9406d19..0479fa2 100644 --- a/login/views.py +++ b/login/views.py @@ -1,13 +1,9 @@ # 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 @@ -19,29 +15,10 @@ from .utils import * 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 # +AUTH_SCOPE = ['user-library-read', 'user-read-recently-played',] # index {{{ # -# Create your views here. def index(request): return render(request, 'login/index.html') @@ -62,7 +39,7 @@ def spotify_login(request): 'response_type': 'code', 'redirect_uri': 'http://localhost:8000/login/callback', 'state': state_str, - 'scope': 'user-library-read', + 'scope': " ".join(AUTH_SCOPE), 'show_dialog': False } @@ -72,6 +49,8 @@ def spotify_login(request): # }}} spotify_login # +# callback {{{ # + def callback(request): """ Step 2 in authorization flow: Have your application request refresh and access tokens; Spotify returns access and refresh tokens. @@ -94,40 +73,13 @@ def callback(request): user_obj = create_user(token_response['refresh_token'], token_response['access_token'], token_response['expires_in']) + + request.session['user_id'] = user_obj.id + request.session['user_secret'] = user_obj.secret - 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'] + return render(request, 'login/scan.html', get_scan_context(request)) - 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 +# }}} callback # # admin_graphs {{{ # @@ -136,7 +88,21 @@ def admin_graphs(request): """ user_id = "polarbier" # user_id = "chrisshyi13" + + request.session['user_id'] = user_id + # request.session['user_secret'] = user_obj.secret + request.session['user_secret'] = User.objects.get(id=user_id).secret user_obj = User.objects.get(id=user_id) return render(request, 'graphs/logged_in.html', get_user_context(user_obj)) # }}} admin_graphs # + +def upload_history(request): + if request.method == 'POST': + form = HistoryUploadForm(request.POST, request.FILES) + if form.is_valid(): + upload_obj = form.save() + # return redirect('graphs:display_history_table') + return redirect('api:import_history', upload_id=upload_obj.id) + + return render(request, 'login/scan.html', get_scan_context(request)) diff --git a/requirements.txt b/requirements.txt index 59c9dc6..23befcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,19 @@ astroid==1.6.3 certifi==2018.4.16 chardet==3.0.4 Django==2.0.5 +django-filter==2.0 djangorestframework==3.8.2 +django-tables2==2.0.2 idna==2.6 isort==4.3.4 lazy-object-proxy==1.3.1 mccabe==0.6.1 psycopg2-binary==2.7.4 pylint==1.8.4 +python-dateutil==2.7.5 pytz==2018.4 requests==2.18.4 six==1.11.0 +tablib==0.12.1 urllib3==1.22 wrapt==1.10.11 diff --git a/spotifyvis/settings.py b/spotifyvis/settings.py index ffbffaa..4c85115 100644 --- a/spotifyvis/settings.py +++ b/spotifyvis/settings.py @@ -40,6 +40,8 @@ INSTALLED_APPS = [ 'login.apps.LoginConfig', 'api.apps.ApiConfig', 'graphs.apps.GraphsConfig', + 'django_tables2', + 'django_filters', ] MIDDLEWARE = [ @@ -128,3 +130,6 @@ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') diff --git a/static/css/dark_bg.css b/static/css/dark_bg.css index a472959..2154bc6 100644 --- a/static/css/dark_bg.css +++ b/static/css/dark_bg.css @@ -1,8 +1,15 @@ body { -background-color: #1e1e1e; + /* dark grey */ + background-color: #1e1e1e; } -h1,p { -color: grey; +h1,th { + /* light grey */ + color: #cccccc; +} + +p,td { + /* light-dark grey */ + color: #b2b2b2; } diff --git a/update-history.sh b/update-history.sh new file mode 100755 index 0000000..5f2c10d --- /dev/null +++ b/update-history.sh @@ -0,0 +1 @@ +/home/kevin/coding/spotify-lib-vis/bin/python /home/kevin/coding/spotify-lib-vis/src/manage.py update-history >> /home/kevin/coding/spotify-lib-vis/src/api/management/commands/update-history.log