Setup API app, can scan library (#47)
Manually merged console logging from loading-page branch.
This commit is contained in:
@@ -22,15 +22,14 @@ class Genre(models.Model):
|
|||||||
|
|
||||||
# Artist {{{ #
|
# Artist {{{ #
|
||||||
|
|
||||||
|
|
||||||
class Artist(models.Model):
|
class Artist(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Artist"
|
verbose_name = "Artist"
|
||||||
verbose_name_plural = "Artists"
|
verbose_name_plural = "Artists"
|
||||||
|
|
||||||
artist_id = models.CharField(primary_key=True, max_length=MAX_ID)
|
id = models.CharField(primary_key=True, max_length=MAX_ID)
|
||||||
# unique since only storing one genre per artist right now
|
# unique since only storing one genre per artist right now
|
||||||
name = models.CharField(unique=True, max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
genres = models.ManyToManyField(Genre, blank=True)
|
genres = models.ManyToManyField(Genre, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -46,7 +45,7 @@ class Track(models.Model):
|
|||||||
verbose_name = "Track"
|
verbose_name = "Track"
|
||||||
verbose_name_plural = "Tracks"
|
verbose_name_plural = "Tracks"
|
||||||
|
|
||||||
track_id = models.CharField(primary_key=True, max_length=MAX_ID)
|
id = models.CharField(primary_key=True, max_length=MAX_ID)
|
||||||
# artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
|
# artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
|
||||||
artists = models.ManyToManyField(Artist, blank=True)
|
artists = models.ManyToManyField(Artist, blank=True)
|
||||||
year = models.PositiveSmallIntegerField()
|
year = models.PositiveSmallIntegerField()
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Logged In</title>
|
<title>Logged In</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="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}">
|
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{{ user_id }}'s Graphs</h1>
|
<h1>{{ user_id }}'s Graphs</h1>
|
||||||
<a class="btn btn-primary" href="{% url "display_audio_features" user_secret %}"
|
<a class="btn btn-primary" href=""
|
||||||
role="button">Audio Features</a>
|
role="button">Audio Features</a>
|
||||||
<a class="btn btn-primary" href="{% url "display_genre_graph" user_secret %}"
|
<a class="btn btn-primary" href=""
|
||||||
role="button">Genres</a>
|
role="button">Genres</a>
|
||||||
<a class="btn btn-primary" href="{% url "display_artist_graph" user_secret %}" role="button">
|
<a class="btn btn-primary" href="" role="button">
|
||||||
Artists
|
Artists
|
||||||
</a>
|
</a>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from .views import *
|
|||||||
|
|
||||||
app_name = 'api'
|
app_name = 'api'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('scan/<str:user_secret>', get_artist_data),
|
path('scan/<str:user_secret>', parse_library,
|
||||||
|
name='scan'),
|
||||||
path('user_artists/<str:user_secret>', get_artist_data,
|
path('user_artists/<str:user_secret>', get_artist_data,
|
||||||
name='get_artist_data'),
|
name='get_artist_data'),
|
||||||
path('user_genres/<str:user_secret>', get_genre_data,
|
path('user_genres/<str:user_secret>', get_genre_data,
|
||||||
|
|||||||
93
api/utils.py
93
api/utils.py
@@ -2,33 +2,36 @@
|
|||||||
import requests
|
import requests
|
||||||
import math
|
import math
|
||||||
import pprint
|
import pprint
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
from .models import *
|
|
||||||
from django.db.models import Count, Q, F
|
from django.db.models import Count, Q, F
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
import json
|
from django.utils import timezone
|
||||||
|
from .models import *
|
||||||
|
from login.models import User
|
||||||
|
|
||||||
# }}} imports #
|
# }}} imports #
|
||||||
|
|
||||||
USER_TRACKS_LIMIT = 50
|
console_logging = True
|
||||||
ARTIST_LIMIT = 50
|
# console_logging = False
|
||||||
FEATURES_LIMIT = 100
|
artists_genre_processed = 0
|
||||||
# ARTIST_LIMIT = 25
|
features_processed = 0
|
||||||
# FEATURES_LIMIT = 25
|
|
||||||
|
|
||||||
# update_track_genres {{{ #
|
# update_track_genres {{{ #
|
||||||
|
|
||||||
def update_track_genres(user):
|
def update_track_genres(user_obj):
|
||||||
"""Updates user's tracks with the most common genre associated with the
|
"""Updates user_obj's tracks with the most common genre associated with the
|
||||||
songs' artist(s).
|
songs' artist(s).
|
||||||
|
|
||||||
:user: User object who's tracks are being updated.
|
:user_obj: User object who's tracks are being updated.
|
||||||
|
|
||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user_tracks = Track.objects.filter(users__exact=user)
|
tracks_processed = 0
|
||||||
|
user_tracks = Track.objects.filter(users__exact=user_obj)
|
||||||
for track in user_tracks:
|
for track in user_tracks:
|
||||||
# just using this variable to save another call to db
|
# just using this variable to save another call to db
|
||||||
track_artists = track.artists.all()
|
track_artists = track.artists.all()
|
||||||
@@ -44,41 +47,46 @@ def update_track_genres(user):
|
|||||||
track.genre = most_common_genre if most_common_genre is not None \
|
track.genre = most_common_genre if most_common_genre is not None \
|
||||||
else undefined_genre_obj
|
else undefined_genre_obj
|
||||||
track.save()
|
track.save()
|
||||||
# print(track.name, track.genre)
|
tracks_processed += 1
|
||||||
|
|
||||||
|
if console_logging:
|
||||||
|
print("Added '{}' as genre for song #{} - '{}'".format(
|
||||||
|
track.genre,
|
||||||
|
tracks_processed,
|
||||||
|
track.name,
|
||||||
|
))
|
||||||
|
|
||||||
# }}} update_track_genres #
|
# }}} update_track_genres #
|
||||||
|
|
||||||
# save_track_obj {{{ #
|
# save_track_obj {{{ #
|
||||||
|
|
||||||
def save_track_obj(track_dict, artists, top_genre, user):
|
def save_track_obj(track_dict, artists, user_obj):
|
||||||
"""Make an entry in the database for this track if it doesn't exist already.
|
"""Make an entry in the database for this track if it doesn't exist already.
|
||||||
|
|
||||||
:track_dict: dictionary from the API call containing track information.
|
:track_dict: dictionary from the API call containing track information.
|
||||||
:artists: artists of the song, passed in as a list of Artist objects.
|
:artists: artists of the song, passed in as a list of Artist objects.
|
||||||
:top_genre: top genre associated with this track (see get_top_genre).
|
:user_obj: User object for which this Track is to be associated with.
|
||||||
:user: User object for which this Track is to be associated with.
|
|
||||||
|
|
||||||
:returns: (The created/retrieved Track object, created)
|
:returns: (The created/retrieved Track object, created)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
track_query = Track.objects.filter(track_id__exact=track_dict['id'])
|
track_query = Track.objects.filter(id__exact=track_dict['id'])
|
||||||
if len(track_query) != 0:
|
if len(track_query) != 0:
|
||||||
return track_query[0], False
|
return track_query[0], False
|
||||||
else:
|
else:
|
||||||
new_track = Track.objects.create(
|
new_track = Track.objects.create(
|
||||||
track_id=track_dict['id'],
|
id=track_dict['id'],
|
||||||
year=track_dict['album']['release_date'].split('-')[0],
|
year=track_dict['album']['release_date'].split('-')[0],
|
||||||
popularity=int(track_dict['popularity']),
|
popularity=int(track_dict['popularity']),
|
||||||
runtime=int(float(track_dict['duration_ms']) / 1000),
|
runtime=int(float(track_dict['duration_ms']) / 1000),
|
||||||
name=track_dict['name'],
|
name=track_dict['name'],
|
||||||
# genre=top_genre,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# have to add artists and user after saving object since track needs to
|
# have to add artists and user_obj after saving object since track needs to
|
||||||
# have ID before filling in m2m field
|
# have ID before filling in m2m field
|
||||||
for artist in artists:
|
for artist in artists:
|
||||||
new_track.artists.add(artist)
|
new_track.artists.add(artist)
|
||||||
new_track.users.add(user)
|
new_track.users.add(user_obj)
|
||||||
new_track.save()
|
new_track.save()
|
||||||
return new_track, True
|
return new_track, True
|
||||||
|
|
||||||
@@ -96,13 +104,14 @@ def get_audio_features(headers, track_objs):
|
|||||||
|
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
track_ids = str.join(",", [track_obj.track_id for track_obj in track_objs])
|
track_ids = str.join(",", [track_obj.id for track_obj in track_objs])
|
||||||
params = {'ids': track_ids}
|
params = {'ids': track_ids}
|
||||||
features_response = requests.get("https://api.spotify.com/v1/audio-features",
|
features_response = requests.get("https://api.spotify.com/v1/audio-features",
|
||||||
headers=headers,params=params).json()['audio_features']
|
headers=headers,params=params).json()['audio_features']
|
||||||
# pprint.pprint(features_response)
|
# pprint.pprint(features_response)
|
||||||
|
|
||||||
useless_keys = [ "key", "mode", "type", "liveness", "id", "uri", "track_href", "analysis_url", "time_signature", ]
|
useless_keys = [ "key", "mode", "type", "liveness", "id", "uri",
|
||||||
|
"track_href", "analysis_url", "time_signature", ]
|
||||||
for i in range(len(track_objs)):
|
for i in range(len(track_objs)):
|
||||||
if features_response[i] is not None:
|
if features_response[i] is not None:
|
||||||
# Data that we don't need
|
# Data that we don't need
|
||||||
@@ -113,6 +122,12 @@ def get_audio_features(headers, track_objs):
|
|||||||
setattr(cur_features_obj, key, val)
|
setattr(cur_features_obj, key, val)
|
||||||
cur_features_obj.save()
|
cur_features_obj.save()
|
||||||
|
|
||||||
|
if console_logging:
|
||||||
|
global features_processed
|
||||||
|
features_processed += 1
|
||||||
|
print("Added features for song #{} - {}".format(
|
||||||
|
features_processed, track_objs[i].name))
|
||||||
|
|
||||||
# }}} get_audio_features #
|
# }}} get_audio_features #
|
||||||
|
|
||||||
def process_artist_genre(genre_name, artist_obj):
|
def process_artist_genre(genre_name, artist_obj):
|
||||||
@@ -145,7 +160,7 @@ def add_artist_genres(headers, artist_objs):
|
|||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
artist_ids = str.join(",", [artist_obj.artist_id for artist_obj in artist_objs])
|
artist_ids = str.join(",", [artist_obj.id for artist_obj in artist_objs])
|
||||||
params = {'ids': artist_ids}
|
params = {'ids': artist_ids}
|
||||||
artists_response = requests.get('https://api.spotify.com/v1/artists/',
|
artists_response = requests.get('https://api.spotify.com/v1/artists/',
|
||||||
headers=headers, params=params).json()['artists']
|
headers=headers, params=params).json()['artists']
|
||||||
@@ -157,6 +172,12 @@ def add_artist_genres(headers, artist_objs):
|
|||||||
for genre in artists_response[i]['genres']:
|
for genre in artists_response[i]['genres']:
|
||||||
process_artist_genre(genre, artist_objs[i])
|
process_artist_genre(genre, artist_objs[i])
|
||||||
|
|
||||||
|
if console_logging:
|
||||||
|
global artists_genre_processed
|
||||||
|
artists_genre_processed += 1
|
||||||
|
print("Added genres for artist #{} - {}".format(
|
||||||
|
artists_genre_processed, artist_objs[i].name))
|
||||||
|
|
||||||
# }}} add_artist_genres #
|
# }}} add_artist_genres #
|
||||||
|
|
||||||
# get_artists_in_genre {{{ #
|
# get_artists_in_genre {{{ #
|
||||||
@@ -192,3 +213,29 @@ def get_artists_in_genre(user, genre, max_songs):
|
|||||||
return processed_artist_counts
|
return processed_artist_counts
|
||||||
|
|
||||||
# }}} get_artists_in_genre #
|
# }}} get_artists_in_genre #
|
||||||
|
|
||||||
|
def get_user_header(user_obj):
|
||||||
|
"""Returns the authorization string needed to make an API call.
|
||||||
|
|
||||||
|
:user_obj: User to return the auth string for.
|
||||||
|
:returns: the authorization string used for the header in a Spotify API
|
||||||
|
call.
|
||||||
|
|
||||||
|
"""
|
||||||
|
seconds_elapsed = (timezone.now() -
|
||||||
|
user_obj.access_obtained_at).total_seconds()
|
||||||
|
if seconds_elapsed >= user_obj.access_expires_in:
|
||||||
|
req_body = {
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': user_obj.refresh_token,
|
||||||
|
'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=req_body).json()
|
||||||
|
user_obj.access_token = token_response['access_token']
|
||||||
|
user_obj.access_expires_in = token_response['expires_in']
|
||||||
|
user_obj.save()
|
||||||
|
|
||||||
|
return {'Authorization': "Bearer " + user_obj.access_token}
|
||||||
|
|||||||
80
api/views.py
80
api/views.py
@@ -3,49 +3,59 @@
|
|||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
import os
|
|
||||||
import urllib
|
import urllib
|
||||||
import secrets
|
import secrets
|
||||||
import pprint
|
import pprint
|
||||||
import string
|
import string
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from .utils import get_artists_in_genre, update_track_genres
|
from .utils import *
|
||||||
from .models import User, Track, AudioFeatures, Artist
|
from .models import *
|
||||||
|
from login.models import User
|
||||||
|
|
||||||
# }}} imports #
|
# }}} imports #
|
||||||
|
|
||||||
|
USER_TRACKS_LIMIT = 50
|
||||||
|
ARTIST_LIMIT = 50
|
||||||
|
FEATURES_LIMIT = 100
|
||||||
|
# ARTIST_LIMIT = 25
|
||||||
|
# FEATURES_LIMIT = 25
|
||||||
TRACKS_TO_QUERY = 200
|
TRACKS_TO_QUERY = 200
|
||||||
|
|
||||||
|
console_logging = True
|
||||||
|
|
||||||
# parse_library {{{ #
|
# parse_library {{{ #
|
||||||
|
|
||||||
def parse_library(headers, tracks, user):
|
def parse_library(request, user_secret):
|
||||||
"""Scans user's library for certain number of tracks and store the information in a database
|
"""Scans user's library for num_tracks and store the information in a
|
||||||
|
database.
|
||||||
:headers: For API call.
|
|
||||||
:tracks: Number of tracks to get from user's library.
|
|
||||||
:user: a User object representing the user whose library we are parsing
|
|
||||||
|
|
||||||
|
:user_secret: secret for User object who's library is being scanned.
|
||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO: implement importing entire library with 0 as tracks param
|
|
||||||
# keeps track of point to get songs from
|
|
||||||
offset = 0
|
offset = 0
|
||||||
payload = {'limit': str(USER_TRACKS_LIMIT)}
|
payload = {'limit': str(USER_TRACKS_LIMIT)}
|
||||||
artist_genre_queue = []
|
artist_genre_queue = []
|
||||||
features_queue = []
|
features_queue = []
|
||||||
|
user_obj = User.objects.get(secret=user_secret)
|
||||||
|
user_headers = get_user_header(user_obj)
|
||||||
|
|
||||||
# iterate until hit requested num of tracks
|
# create this obj so loop runs at least once
|
||||||
for i in range(0, tracks, USER_TRACKS_LIMIT):
|
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)
|
payload['offset'] = str(offset)
|
||||||
saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
|
saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
|
||||||
headers=headers,
|
headers=user_headers,
|
||||||
params=payload).json()
|
params=payload).json()['items']
|
||||||
|
|
||||||
for track_dict in saved_tracks_response['items']:
|
if console_logging:
|
||||||
|
tracks_processed = 0
|
||||||
|
|
||||||
|
for track_dict in saved_tracks_response:
|
||||||
# add artists {{{ #
|
# add artists {{{ #
|
||||||
|
|
||||||
# update artist info before track so that Track object can reference
|
# update artist info before track so that Track object can reference
|
||||||
@@ -53,22 +63,20 @@ def parse_library(headers, tracks, user):
|
|||||||
track_artists = []
|
track_artists = []
|
||||||
for artist_dict in track_dict['track']['artists']:
|
for artist_dict in track_dict['track']['artists']:
|
||||||
artist_obj, artist_created = Artist.objects.get_or_create(
|
artist_obj, artist_created = Artist.objects.get_or_create(
|
||||||
artist_id=artist_dict['id'],
|
id=artist_dict['id'],
|
||||||
name=artist_dict['name'],)
|
name=artist_dict['name'],)
|
||||||
# only add/tally up artist genres if new
|
# only add/tally up artist genres if new
|
||||||
if artist_created:
|
if artist_created:
|
||||||
artist_genre_queue.append(artist_obj)
|
artist_genre_queue.append(artist_obj)
|
||||||
if len(artist_genre_queue) == ARTIST_LIMIT:
|
if len(artist_genre_queue) == ARTIST_LIMIT:
|
||||||
add_artist_genres(headers, artist_genre_queue)
|
add_artist_genres(user_headers, artist_genre_queue)
|
||||||
artist_genre_queue = []
|
artist_genre_queue = []
|
||||||
track_artists.append(artist_obj)
|
track_artists.append(artist_obj)
|
||||||
|
|
||||||
# }}} add artists #
|
# }}} add artists #
|
||||||
|
|
||||||
# TODO: fix this, don't need any more
|
|
||||||
top_genre = ""
|
|
||||||
track_obj, track_created = save_track_obj(track_dict['track'],
|
track_obj, track_created = save_track_obj(track_dict['track'],
|
||||||
track_artists, top_genre, user)
|
track_artists, user_obj)
|
||||||
|
|
||||||
# add audio features {{{ #
|
# add audio features {{{ #
|
||||||
|
|
||||||
@@ -77,16 +85,18 @@ def parse_library(headers, tracks, user):
|
|||||||
if track_created:
|
if track_created:
|
||||||
features_queue.append(track_obj)
|
features_queue.append(track_obj)
|
||||||
if len(features_queue) == FEATURES_LIMIT:
|
if len(features_queue) == FEATURES_LIMIT:
|
||||||
get_audio_features(headers, features_queue)
|
get_audio_features(user_headers, features_queue)
|
||||||
features_queue = []
|
features_queue = []
|
||||||
|
|
||||||
# }}} add audio features #
|
# }}} add audio features #
|
||||||
|
|
||||||
# temporary console logging
|
if console_logging:
|
||||||
print("#{}-{}: {} - {}".format(offset + 1,
|
tracks_processed += 1
|
||||||
offset + USER_TRACKS_LIMIT,
|
print("Added track #{}: {} - {}".format(
|
||||||
track_obj.artists.first(),
|
offset + tracks_processed,
|
||||||
track_obj.name))
|
track_obj.artists.first(),
|
||||||
|
track_obj.name,
|
||||||
|
))
|
||||||
|
|
||||||
# calculates num_songs with offset + songs retrieved
|
# calculates num_songs with offset + songs retrieved
|
||||||
offset += USER_TRACKS_LIMIT
|
offset += USER_TRACKS_LIMIT
|
||||||
@@ -96,13 +106,19 @@ def parse_library(headers, tracks, user):
|
|||||||
# update remaining artists without genres and songs without features if
|
# update remaining artists without genres and songs without features if
|
||||||
# there are any
|
# there are any
|
||||||
if len(artist_genre_queue) > 0:
|
if len(artist_genre_queue) > 0:
|
||||||
add_artist_genres(headers, artist_genre_queue)
|
add_artist_genres(user_headers, artist_genre_queue)
|
||||||
if len(features_queue) > 0:
|
if len(features_queue) > 0:
|
||||||
get_audio_features(headers, features_queue)
|
get_audio_features(user_headers, features_queue)
|
||||||
|
|
||||||
# }}} clean-up #
|
# }}} clean-up #
|
||||||
|
|
||||||
update_track_genres(user)
|
update_track_genres(user_obj)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'user_id': user_obj.id,
|
||||||
|
'user_secret': user_obj.secret,
|
||||||
|
}
|
||||||
|
return render(request, 'api/logged_in.html', context)
|
||||||
|
|
||||||
# }}} parse_library #
|
# }}} parse_library #
|
||||||
|
|
||||||
|
|||||||
104
graphs/models.py
104
graphs/models.py
@@ -1,104 +0,0 @@
|
|||||||
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 {{{ #
|
|
||||||
|
|
||||||
|
|
||||||
class Artist(models.Model):
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "Artist"
|
|
||||||
verbose_name_plural = "Artists"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# }}} Artist #
|
|
||||||
|
|
||||||
# User {{{ #
|
|
||||||
|
|
||||||
class User(models.Model):
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "User"
|
|
||||||
verbose_name_plural = "Users"
|
|
||||||
|
|
||||||
user_id = models.CharField(primary_key=True, max_length=MAX_ID) # the user's Spotify ID
|
|
||||||
user_secret = models.CharField(max_length=50, default='')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.user_id
|
|
||||||
|
|
||||||
# }}} User #
|
|
||||||
|
|
||||||
# Track {{{ #
|
|
||||||
|
|
||||||
class Track(models.Model):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "Track"
|
|
||||||
verbose_name_plural = "Tracks"
|
|
||||||
|
|
||||||
track_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()
|
|
||||||
popularity = models.PositiveSmallIntegerField()
|
|
||||||
runtime = models.PositiveSmallIntegerField()
|
|
||||||
name = models.CharField(max_length=200)
|
|
||||||
users = models.ManyToManyField(User, blank=True)
|
|
||||||
genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
track_str = "{}, genre: {}, artists: [".format(self.name, self.genre)
|
|
||||||
for artist in self.artists.all():
|
|
||||||
track_str += "{}, ".format(artist.name)
|
|
||||||
track_str += "]"
|
|
||||||
return track_str
|
|
||||||
|
|
||||||
# }}} Track #
|
|
||||||
|
|
||||||
# AudioFeatures {{{ #
|
|
||||||
|
|
||||||
class AudioFeatures(models.Model):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "AudioFeatures"
|
|
||||||
verbose_name_plural = "AudioFeatures"
|
|
||||||
|
|
||||||
track = models.OneToOneField(Track, on_delete=models.CASCADE, primary_key=True,)
|
|
||||||
acousticness = models.DecimalField(decimal_places=3, max_digits=3)
|
|
||||||
danceability = models.DecimalField(decimal_places=3, max_digits=3)
|
|
||||||
energy = models.DecimalField(decimal_places=3, max_digits=3)
|
|
||||||
instrumentalness = models.DecimalField(decimal_places=3, max_digits=3)
|
|
||||||
loudness = models.DecimalField(decimal_places=3, max_digits=6)
|
|
||||||
speechiness = models.DecimalField(decimal_places=3, max_digits=3)
|
|
||||||
tempo = models.DecimalField(decimal_places=3, max_digits=6)
|
|
||||||
valence = models.DecimalField(decimal_places=3, max_digits=3)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return super(AudioFeatures, self).__str__()
|
|
||||||
|
|
||||||
# }}} AudioFeatures #
|
|
||||||
@@ -15,8 +15,8 @@ class User(models.Model):
|
|||||||
secret = models.CharField(max_length=50, default='')
|
secret = models.CharField(max_length=50, default='')
|
||||||
refresh_token = models.CharField(max_length=TOKEN_LENGTH)
|
refresh_token = models.CharField(max_length=TOKEN_LENGTH)
|
||||||
access_token = models.CharField(max_length=TOKEN_LENGTH)
|
access_token = models.CharField(max_length=TOKEN_LENGTH)
|
||||||
access_obtained_at = models.DateTimeField(auto_now_add=True)
|
access_obtained_at = models.DateTimeField(auto_now=True)
|
||||||
access_expires_in = models.PositiveIntegerField()
|
access_expires_in = models.PositiveIntegerField()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user_id
|
return self.id
|
||||||
|
|||||||
@@ -10,13 +10,16 @@
|
|||||||
<title>User Spotify Data</title>
|
<title>User Spotify Data</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
|
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!--[if lt IE 7]>
|
<!--[if lt IE 7]>
|
||||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
|
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<p>Logged in as {{ user_id }}</p>
|
<h1>Logged in as {{ user_id }}</h1>
|
||||||
<a href="" class="btn btn-primary">Scan Library</a>
|
<a href="{% url "api:scan" user_secret %}" class="btn btn-primary">
|
||||||
|
Scan Library
|
||||||
|
</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -38,20 +38,6 @@ def generate_random_string(length):
|
|||||||
|
|
||||||
# }}} generate_random_string #
|
# }}} 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 {{{ #
|
# index {{{ #
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
@@ -146,27 +132,6 @@ def create_user(refresh_token, access_token, access_expires_in):
|
|||||||
|
|
||||||
return user_obj
|
return user_obj
|
||||||
|
|
||||||
# refresh access 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']
|
|
||||||
"""
|
|
||||||
|
|
||||||
# }}} refresh access token #
|
|
||||||
|
|
||||||
# admin_graphs {{{ #
|
# admin_graphs {{{ #
|
||||||
|
|
||||||
def admin_graphs(request):
|
def admin_graphs(request):
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ rm login/migrations/0* api/migrations/0* graphs/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
|
||||||
|
python manage.py runserver
|
||||||
# fi
|
# fi
|
||||||
|
|||||||
Reference in New Issue
Block a user