Graphs and tables for your Spotify account.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
8.9 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. # imports {{{ #
  2. import math
  3. import random
  4. import requests
  5. import os
  6. import urllib
  7. import secrets
  8. import pprint
  9. import string
  10. from datetime import datetime
  11. from django.shortcuts import render, redirect
  12. from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
  13. from django.db.models import Count, Q
  14. from .utils import parse_library, get_artists_in_genre, update_track_genres
  15. from .models import User, Track, AudioFeatures, Artist
  16. # }}} imports #
  17. TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
  18. TRACKS_TO_QUERY = 200
  19. # generate_random_string {{{ #
  20. def generate_random_string(length):
  21. """Generates a random string of a certain length
  22. Args:
  23. length: the desired length of the randomized string
  24. Returns:
  25. A random string
  26. """
  27. all_chars = string.ascii_letters + string.digits
  28. rand_str = "".join(random.choice(all_chars) for _ in range(length))
  29. return rand_str
  30. # }}} generate_random_string #
  31. # token_expired {{{ #
  32. def token_expired(token_obtained_at, valid_for):
  33. """Returns True if token expired, False if otherwise
  34. Args:
  35. token_obtained_at: datetime object representing the date and time when the token was obtained
  36. valid_for: the time duration for which the token is valid, in seconds
  37. """
  38. time_elapsed = (datetime.today() - token_obtained_at).total_seconds()
  39. return time_elapsed >= valid_for
  40. # }}} token_expired #
  41. # index {{{ #
  42. # Create your views here.
  43. def index(request):
  44. return render(request, 'spotifyvis/index.html')
  45. # }}} index #
  46. # login {{{ #
  47. # uses Authorization Code flow
  48. def login(request):
  49. # use a randomly generated state string to prevent cross-site request forgery attacks
  50. state_str = generate_random_string(16)
  51. request.session['state_string'] = state_str
  52. payload = {
  53. 'client_id': os.environ['SPOTIFY_CLIENT_ID'],
  54. 'response_type': 'code',
  55. 'redirect_uri': 'http://localhost:8000/callback',
  56. 'state': state_str,
  57. 'scope': 'user-library-read',
  58. 'show_dialog': False
  59. }
  60. params = urllib.parse.urlencode(payload) # turn the payload dict into a query string
  61. authorize_url = "https://accounts.spotify.com/authorize/?{}".format(params)
  62. return redirect(authorize_url)
  63. # }}} login #
  64. # callback {{{ #
  65. def callback(request):
  66. # Attempt to retrieve the authorization code from the query string
  67. try:
  68. code = request.GET['code']
  69. except KeyError:
  70. return HttpResponseBadRequest("<h1>Problem with login</h1>")
  71. payload = {
  72. 'grant_type': 'authorization_code',
  73. 'code': code,
  74. 'redirect_uri': 'http://localhost:8000/callback',
  75. 'client_id': os.environ['SPOTIFY_CLIENT_ID'],
  76. 'client_secret': os.environ['SPOTIFY_CLIENT_SECRET'],
  77. }
  78. response = requests.post('https://accounts.spotify.com/api/token', data=payload).json()
  79. # despite its name, datetime.today() returns a datetime object, not a date object
  80. # use datetime.strptime() to get a datetime object from a string
  81. request.session['token_obtained_at'] = datetime.strftime(datetime.today(), TIME_FORMAT)
  82. request.session['access_token'] = response['access_token']
  83. request.session['refresh_token'] = response['refresh_token']
  84. request.session['valid_for'] = response['expires_in']
  85. # print(response)
  86. return redirect('user_data')
  87. # }}} callback #
  88. # user_data {{{ #
  89. def user_data(request):
  90. # get user token {{{ #
  91. token_obtained_at = datetime.strptime(request.session['token_obtained_at'], TIME_FORMAT)
  92. valid_for = int(request.session['valid_for'])
  93. if token_expired(token_obtained_at, valid_for):
  94. req_body = {
  95. 'grant_type': 'refresh_token',
  96. 'refresh_token': request.session['refresh_token'],
  97. 'client_id': os.environ['SPOTIFY_CLIENT_ID'],
  98. 'client_secret': os.environ['SPOTIFY_CLIENT_SECRET']
  99. }
  100. refresh_token_response = requests.post('https://accounts.spotify.com/api/token', data=req_body).json()
  101. request.session['access_token'] = refresh_token_response['access_token']
  102. request.session['valid_for'] = refresh_token_response['expires_in']
  103. # }}} get user token #
  104. auth_token_str = "Bearer " + request.session['access_token']
  105. headers = {
  106. 'Authorization': auth_token_str
  107. }
  108. user_data_response = requests.get('https://api.spotify.com/v1/me', headers = headers).json()
  109. # store the user_id so it may be used to create model
  110. request.session['user_id'] = user_data_response['id']
  111. # create user obj {{{ #
  112. try:
  113. user = User.objects.get(user_id=user_data_response['id'])
  114. except User.DoesNotExist:
  115. # Python docs recommends 32 bytes of randomness against brute force attacks
  116. user = User(user_id=user_data_response['id'], user_secret=secrets.token_urlsafe(32))
  117. request.session['user_secret'] = user.user_secret
  118. user.save()
  119. # }}} create user obj #
  120. context = {
  121. 'user_id': user.user_id,
  122. 'user_secret': user.user_secret,
  123. }
  124. parse_library(headers, TRACKS_TO_QUERY, user)
  125. return render(request, 'spotifyvis/logged_in.html', context)
  126. # }}} user_data #
  127. def admin_graphs(request):
  128. """TODO
  129. """
  130. user_id = "polarbier"
  131. # user_id = "chrisshyi13"
  132. user_obj = User.objects.get(user_id=user_id)
  133. context = {
  134. 'user_id': user_id,
  135. 'user_secret': user_obj.user_secret,
  136. }
  137. update_track_genres(user_obj)
  138. return render(request, 'spotifyvis/logged_in.html', context)
  139. def artist_data(request, user_secret):
  140. """Renders the artist data graph display page
  141. :param request: the HTTP request
  142. :param user_secret: the user secret used for identification
  143. :return: render the artist data graph display page
  144. """
  145. user = User.objects.get(user_secret=user_secret)
  146. context = {
  147. 'user_id': user.user_id,
  148. 'user_secret': user_secret,
  149. }
  150. return render(request, "spotifyvis/artist_graph.html", context)
  151. # get_artist_data {{{ #
  152. def get_artist_data(request, user_secret):
  153. """Returns artist data as a JSON serialized list of dictionaries
  154. The (key, value) pairs are (artist name, song count for said artist)
  155. :param request: the HTTP request
  156. :param user_secret: the user secret used for identification
  157. :return: a JsonResponse
  158. """
  159. user = User.objects.get(user_secret=user_secret)
  160. artist_counts = Artist.objects.annotate(num_songs=Count('track',
  161. filter=Q(track__users=user)))
  162. processed_artist_counts = [{'name': artist.name,
  163. 'num_songs': artist.num_songs} for artist in artist_counts]
  164. return JsonResponse(data=processed_artist_counts, safe=False)
  165. # }}} get_artist_data #
  166. def display_genre_graph(request, user_secret):
  167. user = User.objects.get(user_secret=user_secret)
  168. context = {
  169. 'user_secret': user_secret,
  170. }
  171. return render(request, "spotifyvis/genre_graph.html", context)
  172. def audio_features(request, user_secret):
  173. """Renders the audio features page
  174. :param request: the HTTP request
  175. :param user_secret: user secret used for identification
  176. :return: renders the audio features page
  177. """
  178. user = User.objects.get(user_secret=user_secret)
  179. context = {
  180. 'user_id': user.user_id,
  181. 'user_secret': user_secret,
  182. }
  183. return render(request, "spotifyvis/audio_features.html", context)
  184. # get_audio_feature_data {{{ #
  185. def get_audio_feature_data(request, audio_feature, user_secret):
  186. """Returns all data points for a given audio feature
  187. Args:
  188. request: the HTTP request
  189. audio_feature: The audio feature to be queried
  190. user_secret: client secret, used to identify the user
  191. """
  192. user = User.objects.get(user_secret=user_secret)
  193. user_tracks = Track.objects.filter(users=user)
  194. response_payload = {
  195. 'data_points': [],
  196. }
  197. for track in user_tracks:
  198. try:
  199. audio_feature_obj = AudioFeatures.objects.get(track=track)
  200. response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature))
  201. except AudioFeatures.DoesNotExist:
  202. continue
  203. return JsonResponse(response_payload)
  204. # }}} get_audio_feature_data #
  205. # get_genre_data {{{ #
  206. def get_genre_data(request, user_secret):
  207. """Return genre data needed to create the graph user.
  208. TODO
  209. """
  210. user = User.objects.get(user_secret=user_secret)
  211. genre_counts = (Track.objects.filter(users__exact=user)
  212. .values('genre')
  213. .order_by('genre')
  214. .annotate(num_songs=Count('genre'))
  215. )
  216. for genre_dict in genre_counts:
  217. genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'],
  218. genre_dict['num_songs'])
  219. print("*** Genre Breakdown ***")
  220. pprint.pprint(list(genre_counts))
  221. return JsonResponse(data=list(genre_counts), safe=False)
  222. # }}} get_genre_data #