Browse Source

Merge pull request #64 from Kevin-Mok/history

Scan user history
master
Kevin Mok 5 years ago
committed by GitHub
parent
commit
db184da0a2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 10
      api/management/commands/update-history.py
  3. 30
      api/models.py
  4. 8
      api/urls.py
  5. 178
      api/utils.py
  6. 139
      api/views.py
  7. 12
      graphs/templates/graphs/genre_graph.html
  8. 3
      graphs/templates/graphs/logged_in.html
  9. 17
      graphs/templates/graphs/user_history.html
  10. 1
      graphs/urls.py
  11. 16
      graphs/utils.py
  12. 54
      graphs/views.py
  13. 8
      login/forms.py
  14. 5
      login/models.py
  15. 2
      login/templates/login/index.html
  16. 10
      login/templates/login/scan.html
  17. 1
      login/urls.py
  18. 73
      login/utils.py
  19. 80
      login/views.py
  20. 4
      requirements.txt
  21. 5
      spotifyvis/settings.py
  22. 13
      static/css/dark_bg.css
  23. 1
      update-history.sh

2
.gitignore

@ -1,4 +1,5 @@
*.pyc
*.log
db.sqlite3
*.bak
.idea/
@ -9,3 +10,4 @@ api-keys.sh
Pipfile
*.txt
scrap.py
media/history/*

10
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)

30
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()
# }}} #

8
api/urls.py

@ -4,12 +4,16 @@ from .views import *
app_name = 'api'
urlpatterns = [
path('scan/<str:user_secret>', parse_library,
name='scan'),
path('scan/library/<str:user_secret>', parse_library,
name='scan_library'),
path('scan/history/<str:user_secret>', parse_history_request,
name='scan_history'),
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'),
path('import/history/<upload_id>', import_history, name='import_history'),
]

178
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 #

139
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 #

12
graphs/templates/graphs/genre_graph.html

@ -21,10 +21,18 @@
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/randomcolor/0.5.2/randomColor.min.js"></script>
{% load static %}
<script src="{% static "graphs/scripts/genre_graph.js" %}"></script>
<svg width="1920" height="740"></svg>
<!-- <div class="row">
<div class="col-" id="genre-graph"></div>
</div> -->
<svg id="genre-graph" width="600" height="400"
viewBox="0 0 600 400"
perserveAspectRatio="xMinYMid">
</svg>
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 40},
@ -38,7 +46,7 @@
var y = d3.scaleLinear()
.rangeRound([height, 0]);
d3.json("{% url "api:get_genre_data" user_secret %}").then(create_genre_graph);
d3.json("{% url "api:get_genre_data" user_secret %}").then(create_genre_graph);
</script>
</body>
</html>

3
graphs/templates/graphs/logged_in.html

@ -16,5 +16,8 @@
<a class="btn btn-primary" href="{% url "graphs:display_artist_graph" user_secret %}" role="button">
Artists
</a>
<a class="btn btn-primary" href="{% url "graphs:display_history_table" %}" role="button">
History
</a>
</body>
</html>

17
graphs/templates/graphs/user_history.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
{% load static %}
{% load render_table export_url from django_tables2 %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>User History</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
</head>
<body>
<h1>{{ user_id }}'s Listening History</h1>
<p> Found {{ total_history }} songs. </p>
<a class="btn btn-primary " href="{% export_url 'csv' %}" role="button">Export</a>
{% render_table user_history_table %}
</body>
</html>

1
graphs/urls.py

@ -10,4 +10,5 @@ urlpatterns = [
name='display_genre_graph'),
path('audio_features/<str:user_secret>', display_features_graphs,
name='display_audio_features'),
path('history/', HistoryList.as_view(), name='display_history_table'),
]

16
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.

54
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 #

8
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() }

5
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)

2
login/templates/login/index.html

@ -20,7 +20,7 @@
<div class="container">
<div id="login">
<h1>spotify-lib-vis</h1>
<a href="{% url 'login:spotify_login' %}" class="btn btn-primary">Login</a>
<a href="{% url 'login:spotify_login' %}" class="btn btn-primary" role="button">Login</a>
<a href="{% url 'login:admin_graphs' %}" class="btn btn-primary">Admin Graphs</a>
</div>
</div>

10
login/templates/login/scan.html

@ -18,8 +18,16 @@
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<h1>Logged in as {{ user_id }}</h1>
<a href="{% url "api:scan" user_secret %}" class="btn btn-primary">
<a href="{% url "api:scan_library" user_secret %}" class="btn btn-primary">
Scan Library
</a>
<a href="{% url "api:scan_history" user_secret %}" class="btn btn-primary">
Scan History
</a>
<form action="{% url 'login:upload_history' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<input class="btn btn-primary" type="submit" value="Import History">
</form>
</body>
</html>

1
login/urls.py

@ -9,4 +9,5 @@ urlpatterns = [
path('callback', callback, name='callback'),
# path('user/<str:user_secret>', user_home, name='user_home'),
path('admin_graphs', admin_graphs, name='admin_graphs'),
path('upload_history', upload_history, name='upload_history'),
]

73
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

80
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))

4
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

5
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')

13
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;
}

1
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
Loading…
Cancel
Save