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.

276 lines
9.0 KiB

  1. from django.shortcuts import render, redirect
  2. from django.http import HttpResponse, HttpResponseBadRequest
  3. import math
  4. import random
  5. import requests
  6. import os
  7. import urllib
  8. import json
  9. import pprint
  10. from datetime import datetime
  11. TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
  12. library_stats = {"audio_features":{}, "genres":{}, "year_released":{}, "artists":{}, "num_songs":0, "popularity":[], "total_runtime":0}
  13. # generate_random_string {{{ #
  14. def generate_random_string(length):
  15. """Generates a random string of a certain length
  16. Args:
  17. length: the desired length of the randomized string
  18. Returns:
  19. A random string
  20. """
  21. rand_str = ""
  22. possible_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
  23. for _ in range(length):
  24. rand_str += possible_chars[random.randint(0, len(possible_chars) - 1)]
  25. return rand_str
  26. # }}} generate_random_string #
  27. # token_expired {{{ #
  28. def token_expired(token_obtained_at, valid_for):
  29. """Returns True if token expired, False if otherwise
  30. Args:
  31. token_obtained_at: datetime object representing the date and time when the token was obtained
  32. valid_for: the time duration for which the token is valid, in seconds
  33. """
  34. time_elapsed = (datetime.today() - token_obtained_at).total_seconds()
  35. return time_elapsed >= valid_for
  36. # }}} token_expired #
  37. # index {{{ #
  38. # Create your views here.
  39. def index(request):
  40. return render(request, 'spotifyvis/index.html')
  41. # }}} index #
  42. # login {{{ #
  43. def login(request):
  44. # use a randomly generated state string to prevent cross-site request forgery attacks
  45. state_str = generate_random_string(16)
  46. request.session['state_string'] = state_str
  47. payload = {
  48. 'client_id': os.environ['SPOTIFY_CLIENT_ID'],
  49. 'response_type': 'code',
  50. 'redirect_uri': 'http://localhost:8000/callback',
  51. 'state': state_str,
  52. 'scope': 'user-library-read',
  53. 'show_dialog': False
  54. }
  55. params = urllib.parse.urlencode(payload) # turn the payload dict into a query string
  56. authorize_url = "https://accounts.spotify.com/authorize/?{}".format(params)
  57. return redirect(authorize_url)
  58. # }}} login #
  59. # callback {{{ #
  60. def callback(request):
  61. # Attempt to retrieve the authorization code from the query string
  62. try:
  63. code = request.GET['code']
  64. except KeyError:
  65. return HttpResponseBadRequest("<h1>Problem with login</h1>")
  66. payload = {
  67. 'grant_type': 'authorization_code',
  68. 'code': code,
  69. 'redirect_uri': 'http://localhost:8000/callback',
  70. 'client_id': os.environ['SPOTIFY_CLIENT_ID'],
  71. 'client_secret': os.environ['SPOTIFY_CLIENT_SECRET'],
  72. }
  73. response = requests.post('https://accounts.spotify.com/api/token', data = payload).json()
  74. # despite its name, datetime.today() returns a datetime object, not a date object
  75. # use datetime.strptime() to get a datetime object from a string
  76. request.session['token_obtained_at'] = datetime.strftime(datetime.today(), TIME_FORMAT)
  77. request.session['access_token'] = response['access_token']
  78. request.session['refresh_token'] = response['refresh_token']
  79. request.session['valid_for'] = response['expires_in']
  80. # print(response)
  81. return redirect('user_data')
  82. # }}} callback #
  83. # user_data {{{ #
  84. def user_data(request):
  85. token_obtained_at = datetime.strptime(request.session['token_obtained_at'], TIME_FORMAT)
  86. valid_for = int(request.session['valid_for'])
  87. if token_expired(token_obtained_at, valid_for):
  88. req_body = {
  89. 'grant_type': 'refresh_token',
  90. 'refresh_token': request.session['refresh_token'],
  91. 'client_id': os.environ['SPOTIFY_CLIENT_ID'],
  92. 'client_secret': os.environ['SPOTIFY_CLIENT_SECRET']
  93. }
  94. refresh_token_response = requests.post('https://accounts.spotify.com/api/token', data = req_body).json()
  95. request.session['access_token'] = refresh_token_response['access_token']
  96. request.session['valid_for'] = refresh_token_response['expires_in']
  97. auth_token_str = "Bearer " + request.session['access_token']
  98. headers = {
  99. 'Authorization': auth_token_str
  100. }
  101. user_data_response = requests.get('https://api.spotify.com/v1/me', headers = headers).json()
  102. context = {
  103. 'user_name': user_data_response['display_name'],
  104. 'id': user_data_response['id'],
  105. }
  106. tracks_to_query = 50
  107. parse_library(headers, tracks_to_query)
  108. return render(request, 'spotifyvis/user_data.html', context)
  109. # }}} user_data #
  110. # parse_library {{{ #
  111. def parse_library(headers, tracks):
  112. """Scans user's library for certain number of tracks to update library_stats with.
  113. :headers: For API call.
  114. :tracks: Number of tracks to get from user's library.
  115. :returns: None
  116. """
  117. # TODO: implement importing entire library with 0 as tracks param
  118. # number of tracks to get with each call
  119. limit = 50
  120. # keeps track of point to get songs from
  121. offset = 0
  122. payload = {'limit': str(limit)}
  123. for i in range(0, tracks, limit):
  124. payload['offset'] = str(offset)
  125. saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks', headers=headers, params=payload).json()
  126. for track_dict in saved_tracks_response['items']:
  127. get_track_info(track_dict['track'])
  128. # get_genre(headers, track_dict['track']['album']['id'])
  129. for artist_dict in track_dict['track']['artists']:
  130. increase_artist_count(headers, artist_dict['name'], artist_dict['id'])
  131. # calculates num_songs with offset + songs retrieved
  132. library_stats['num_songs'] = offset + len(saved_tracks_response['items'])
  133. offset += limit
  134. pprint.pprint(library_stats)
  135. # }}} parse_library #
  136. # increase_nested_key {{{ #
  137. def increase_nested_key(top_key, nested_key):
  138. """Increases count for the value of library_stats[top_key][nested_key]. Checks if nested_key exists already and takes
  139. appropriate action.
  140. :top_key: First key of library_stats.
  141. :nested_key: Key in top_key's dict for which we want to increase value of.
  142. :returns: None
  143. """
  144. if nested_key not in library_stats[top_key]:
  145. library_stats[top_key][nested_key] = 1
  146. else:
  147. library_stats[top_key][nested_key] += 1
  148. # }}} increase_nested_key #
  149. # increase_artist_count {{{ #
  150. def increase_artist_count(headers, artist_name, artist_id):
  151. """Increases count for artist and genre in library_stats. Also looks up genre of artist if new key.
  152. :headers: For making the API call.
  153. :artist_name: Artist to increase count for.
  154. :artist_id: The Spotify ID for the artist.
  155. :returns: None
  156. """
  157. if artist_name not in library_stats['artists']:
  158. library_stats['artists'][artist_name] = {}
  159. library_stats['artists'][artist_name]['count'] = 1
  160. # set genres for artist
  161. artist_response = requests.get('https://api.spotify.com/v1/artists/' + artist_id, headers=headers).json()
  162. library_stats['artists'][artist_name]['genres'] = artist_response['genres']
  163. else:
  164. library_stats['artists'][artist_name]['count'] += 1
  165. # update genre counts
  166. for genre in library_stats['artists'][artist_name]['genres']:
  167. increase_nested_key('genres', genre)
  168. # }}} increase_artist_count #
  169. # get_track_info {{{ #
  170. def get_track_info(track_dict):
  171. """Get all the info from the track_dict directly returned by the API call in parse_library.
  172. :track_dict: Dict returned from the API call containing the track info.
  173. :returns: None
  174. """
  175. # popularity
  176. library_stats['popularity'].append(track_dict['popularity'])
  177. # year
  178. year_released = track_dict['album']['release_date'].split('-')[0]
  179. increase_nested_key('year_released', year_released)
  180. # artist
  181. # artist_names = [artist['name'] for artist in track_dict['artists']]
  182. # for artist_name in artist_names:
  183. # increase_nested_key('artists', artist_name)
  184. # runtime
  185. library_stats['total_runtime'] += float(track_dict['duration_ms']) / 60
  186. # }}} get_track_info #
  187. # get_genre {{{ #
  188. # Deprecated. Will remove in next commit. I queried 300 albums and none of them had genres.
  189. # The organization app gets the genre from the artist, and I've implemented other functions
  190. # to do the same.
  191. def get_genre(headers, album_id):
  192. """Updates library_stats with this track's genre.
  193. :headers: For making the API call.
  194. :album_id: The Spotify ID for the album.
  195. :returns: None
  196. """
  197. album_response = requests.get('https://api.spotify.com/v1/albums/' + album_id, headers=headers).json()
  198. pprint.pprint(album_response['genres'])
  199. for genre in album_response['genres']:
  200. # print(genre)
  201. increase_nested_key('genres', genre);
  202. # }}} get_genre #
  203. # def calculate_genres_from_artists(headers):
  204. # """Tallies up genre counts based on artists in library_stats.
  205. # :headers: For making the API call.
  206. # :returns: None
  207. # """
  208. # album_response = requests.get('https://api.spotify.com/v1/albums/' + album_id, headers=headers).json()