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.

263 lines
8.7 KiB

  1. # imports {{{ #
  2. import math
  3. import random
  4. import requests
  5. import urllib
  6. import secrets
  7. import string
  8. from django.shortcuts import render, redirect
  9. from django.http import JsonResponse
  10. from django.db.models import Count, Q, Max
  11. from .utils import *
  12. from .models import *
  13. from login.models import User
  14. from login.utils import get_user_context
  15. from dateutil.parser import parse
  16. from pprint import pprint
  17. # }}} imports #
  18. # constants {{{ #
  19. USER_TRACKS_LIMIT = 50
  20. HISTORY_LIMIT = 50
  21. ARTIST_LIMIT = 50
  22. FEATURES_LIMIT = 100
  23. # ARTIST_LIMIT = 25
  24. # FEATURES_LIMIT = 25
  25. TRACKS_TO_QUERY = 100
  26. HISTORY_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played'
  27. console_logging = True
  28. # console_logging = False
  29. # }}} constants #
  30. # parse_library {{{ #
  31. def parse_library(request, user_secret):
  32. """Scans user's library for num_tracks and store the information in a
  33. database.
  34. :user_secret: secret for User object who's library is being scanned.
  35. :returns: None
  36. """
  37. offset = 0
  38. payload = {'limit': str(USER_TRACKS_LIMIT)}
  39. artist_genre_queue = []
  40. features_queue = []
  41. user_obj = User.objects.get(secret=user_secret)
  42. user_headers = get_user_header(user_obj)
  43. # create this obj so loop runs at least once
  44. saved_tracks_response = [0]
  45. # scan until reach num_tracks or no tracks left if scanning entire library
  46. while (TRACKS_TO_QUERY == 0 or offset < TRACKS_TO_QUERY) and len(saved_tracks_response) > 0:
  47. payload['offset'] = str(offset)
  48. saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
  49. headers=user_headers,
  50. params=payload).json()['items']
  51. if console_logging:
  52. tracks_processed = 0
  53. for track_dict in saved_tracks_response:
  54. # add artists {{{ #
  55. # update artist info before track so that Track object can reference
  56. # Artist object
  57. track_artists = []
  58. for artist_dict in track_dict['track']['artists']:
  59. artist_obj, artist_created = Artist.objects.get_or_create(
  60. id=artist_dict['id'],
  61. name=artist_dict['name'],)
  62. # only add/tally up artist genres if new
  63. if artist_created:
  64. artist_genre_queue.append(artist_obj)
  65. if len(artist_genre_queue) == ARTIST_LIMIT:
  66. add_artist_genres(user_headers, artist_genre_queue)
  67. artist_genre_queue = []
  68. track_artists.append(artist_obj)
  69. # }}} add artists #
  70. track_obj, track_created = save_track_obj(track_dict['track'],
  71. track_artists, user_obj)
  72. # add audio features {{{ #
  73. # if a new track is not created, the associated audio feature does
  74. # not need to be created again
  75. if track_created:
  76. features_queue.append(track_obj)
  77. if len(features_queue) == FEATURES_LIMIT:
  78. get_audio_features(user_headers, features_queue)
  79. features_queue = []
  80. # }}} add audio features #
  81. if console_logging:
  82. tracks_processed += 1
  83. print("Added track #{}: {} - {}".format(
  84. offset + tracks_processed,
  85. track_obj.artists.first(),
  86. track_obj.name,
  87. ))
  88. # calculates num_songs with offset + songs retrieved
  89. offset += USER_TRACKS_LIMIT
  90. # clean-up {{{ #
  91. # update remaining artists without genres and songs without features if
  92. # there are any
  93. if len(artist_genre_queue) > 0:
  94. add_artist_genres(user_headers, artist_genre_queue)
  95. if len(features_queue) > 0:
  96. get_audio_features(user_headers, features_queue)
  97. # }}} clean-up #
  98. update_track_genres(user_obj)
  99. return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
  100. # }}} parse_library #
  101. # parse_history {{{ #
  102. def parse_history(request, user_secret):
  103. """Scans user's listening history and stores the information in a
  104. database.
  105. :user_secret: secret for User object who's library is being scanned.
  106. :returns: None
  107. """
  108. user_obj = User.objects.get(secret=user_secret)
  109. payload = {'limit': str(USER_TRACKS_LIMIT)}
  110. last_time_played = History.objects.filter(user=user_obj).aggregate(Max('timestamp'))['timestamp__max']
  111. if last_time_played is not None:
  112. payload['after'] = last_time_played.isoformat()
  113. artist_genre_queue = []
  114. user_headers = get_user_header(user_obj)
  115. history_response = requests.get(HISTORY_ENDPOINT,
  116. headers=user_headers,
  117. params=payload).json()['items']
  118. # pprint(history_response)
  119. if console_logging:
  120. tracks_processed = 0
  121. for track_dict in history_response:
  122. # add artists {{{ #
  123. # update artist info before track so that Track object can reference
  124. # Artist object
  125. track_artists = []
  126. for artist_dict in track_dict['track']['artists']:
  127. artist_obj, artist_created = Artist.objects.get_or_create(
  128. id=artist_dict['id'],
  129. name=artist_dict['name'],)
  130. # only add/tally up artist genres if new
  131. if artist_created:
  132. artist_genre_queue.append(artist_obj)
  133. if len(artist_genre_queue) == ARTIST_LIMIT:
  134. add_artist_genres(user_headers, artist_genre_queue)
  135. artist_genre_queue = []
  136. track_artists.append(artist_obj)
  137. # }}} add artists #
  138. # don't associate history track with User, not necessarily in their
  139. # library
  140. track_obj, track_created = save_track_obj(track_dict['track'],
  141. track_artists, None)
  142. history_obj, history_created = History.objects.get_or_create(
  143. user=user_obj,
  144. timestamp=parse(track_dict['played_at']),
  145. track=track_obj,)
  146. if console_logging:
  147. tracks_processed += 1
  148. print("Added history track #{}: {}".format(
  149. tracks_processed, history_obj,))
  150. if len(artist_genre_queue) > 0:
  151. add_artist_genres(user_headers, artist_genre_queue)
  152. # TODO: update track genres from History relation
  153. # update_track_genres(user_obj)
  154. return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
  155. # }}} get_history #
  156. # get_artist_data {{{ #
  157. def get_artist_data(request, user_secret):
  158. """Returns artist data as a JSON serialized list of dictionaries
  159. The (key, value) pairs are (artist name, song count for said artist)
  160. :param request: the HTTP request
  161. :param user_secret: the user secret used for identification
  162. :return: a JsonResponse
  163. """
  164. user = User.objects.get(secret=user_secret)
  165. artist_counts = Artist.objects.annotate(num_songs=Count('track',
  166. filter=Q(track__users=user)))
  167. processed_artist_counts = [{'name': artist.name, 'num_songs': artist.num_songs}
  168. for artist in artist_counts]
  169. pprint.pprint(processed_artist_counts)
  170. return JsonResponse(data=processed_artist_counts, safe=False)
  171. # }}} get_artist_data #
  172. # get_audio_feature_data {{{ #
  173. def get_audio_feature_data(request, audio_feature, user_secret):
  174. """Returns all data points for a given audio feature
  175. Args:
  176. request: the HTTP request
  177. audio_feature: The audio feature to be queried
  178. user_secret: client secret, used to identify the user
  179. """
  180. user = User.objects.get(secret=user_secret)
  181. user_tracks = Track.objects.filter(users=user)
  182. response_payload = {
  183. 'data_points': [],
  184. }
  185. for track in user_tracks:
  186. try:
  187. audio_feature_obj = AudioFeatures.objects.get(track=track)
  188. response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature))
  189. except AudioFeatures.DoesNotExist:
  190. continue
  191. return JsonResponse(response_payload)
  192. # }}} get_audio_feature_data #
  193. # get_genre_data {{{ #
  194. def get_genre_data(request, user_secret):
  195. """Return genre data needed to create the graph user.
  196. TODO
  197. """
  198. user = User.objects.get(secret=user_secret)
  199. genre_counts = (Track.objects.filter(users__exact=user)
  200. .values('genre')
  201. .order_by('genre')
  202. .annotate(num_songs=Count('genre'))
  203. )
  204. for genre_dict in genre_counts:
  205. genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'],
  206. genre_dict['num_songs'])
  207. print("*** Genre Breakdown ***")
  208. pprint.pprint(list(genre_counts))
  209. return JsonResponse(data=list(genre_counts), safe=False)
  210. # }}} get_genre_data #