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.

296 lines
9.6 KiB

  1. # imports {{{ #
  2. import math
  3. import random
  4. import requests
  5. import urllib
  6. import secrets
  7. import string
  8. import csv
  9. from django.shortcuts import render, redirect
  10. from django.http import JsonResponse
  11. from django.db.models import Count, Q, Max
  12. from django.core.files import File
  13. from .utils import *
  14. from .models import *
  15. from login.models import User
  16. from login.utils import get_user_context
  17. from dateutil.parser import parse
  18. from pprint import pprint
  19. from login.models import HistoryUpload
  20. # }}} imports #
  21. # constants {{{ #
  22. USER_TRACKS_LIMIT = 50
  23. TRACKS_LIMIT = 50
  24. HISTORY_LIMIT = 50
  25. ARTIST_LIMIT = 50
  26. FEATURES_LIMIT = 100
  27. # ARTIST_LIMIT = 25
  28. # FEATURES_LIMIT = 25
  29. TRACKS_TO_QUERY = 100
  30. TRACKS_ENDPOINT = 'https://api.spotify.com/v1/tracks'
  31. CONSOLE_LOGGING = True
  32. # CONSOLE_LOGGING = False
  33. # }}} constants #
  34. # parse_library {{{ #
  35. def parse_library(request, user_secret):
  36. """Scans user's library for num_tracks and store the information in a
  37. database.
  38. :user_secret: secret for User object who's library is being scanned.
  39. :returns: None
  40. """
  41. offset = 0
  42. payload = {'limit': str(USER_TRACKS_LIMIT)}
  43. artist_genre_queue = []
  44. features_queue = []
  45. user_obj = User.objects.get(secret=user_secret)
  46. user_headers = get_user_header(user_obj)
  47. # create this obj so loop runs at least once
  48. saved_tracks_response = [0]
  49. # scan until reach num_tracks or no tracks left if scanning entire library
  50. while ((TRACKS_TO_QUERY == 0 or offset < TRACKS_TO_QUERY) and
  51. len(saved_tracks_response) > 0):
  52. payload['offset'] = str(offset)
  53. saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
  54. headers=user_headers,
  55. params=payload).json()['items']
  56. tracks_processed = 0
  57. for track_dict in saved_tracks_response:
  58. track_artists = save_track_artists(track_dict['track'], artist_genre_queue,
  59. user_headers)
  60. track_obj, track_created = save_track_obj(track_dict['track'],
  61. track_artists, user_obj)
  62. # add audio features {{{ #
  63. # if a new track is not created, the associated audio feature does
  64. # not need to be created again
  65. if track_created:
  66. features_queue.append(track_obj)
  67. if len(features_queue) == FEATURES_LIMIT:
  68. get_audio_features(user_headers, features_queue)
  69. features_queue = []
  70. # }}} add audio features #
  71. if CONSOLE_LOGGING:
  72. tracks_processed += 1
  73. print("Added track #{}: {} - {}".format(
  74. offset + tracks_processed,
  75. track_obj.artists.first(),
  76. track_obj.name,
  77. ))
  78. # calculates num_songs with offset + songs retrieved
  79. offset += USER_TRACKS_LIMIT
  80. # clean-up {{{ #
  81. # update remaining artists without genres and songs without features if
  82. # there are any
  83. if len(artist_genre_queue) > 0:
  84. add_artist_genres(user_headers, artist_genre_queue)
  85. if len(features_queue) > 0:
  86. get_audio_features(user_headers, features_queue)
  87. # }}} clean-up #
  88. update_track_genres(user_obj)
  89. return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
  90. # }}} parse_library #
  91. # parse_history_request {{{ #
  92. def parse_history_request(request, user_secret):
  93. """Request function to call parse_history. Scans user's listening history
  94. and stores the information in a database.
  95. :user_secret: secret for User object who's library is being scanned.
  96. :returns: redirects user to logged in page
  97. """
  98. parse_history(user_secret)
  99. return render(request, 'graphs/logged_in.html',
  100. get_user_context(User.objects.get(secret=user_secret)))
  101. # }}} get_history #
  102. # get_artist_data {{{ #
  103. def get_artist_data(request, user_secret):
  104. """Returns artist data as a JSON serialized list of dictionaries
  105. The (key, value) pairs are (artist name, song count for said artist)
  106. :param request: the HTTP request
  107. :param user_secret: the user secret used for identification
  108. :return: a JsonResponse
  109. """
  110. user = User.objects.get(secret=user_secret)
  111. artist_counts = Artist.objects.annotate(num_songs=Count('track',
  112. filter=Q(track__users=user)))
  113. processed_artist_counts = [{'name': artist.name, 'num_songs': artist.num_songs}
  114. for artist in artist_counts]
  115. if CONSOLE_LOGGING:
  116. pprint(processed_artist_counts)
  117. return JsonResponse(data=processed_artist_counts, safe=False)
  118. # }}} get_artist_data #
  119. # get_audio_feature_data {{{ #
  120. def get_audio_feature_data(request, audio_feature, user_secret):
  121. """Returns all data points for a given audio feature
  122. Args:
  123. request: the HTTP request
  124. audio_feature: The audio feature to be queried
  125. user_secret: client secret, used to identify the user
  126. """
  127. user = User.objects.get(secret=user_secret)
  128. user_tracks = Track.objects.filter(users=user)
  129. response_payload = {
  130. 'data_points': [],
  131. }
  132. for track in user_tracks:
  133. try:
  134. audio_feature_obj = AudioFeatures.objects.get(track=track)
  135. response_payload['data_points'].append(getattr(audio_feature_obj, audio_feature))
  136. except AudioFeatures.DoesNotExist:
  137. continue
  138. return JsonResponse(response_payload)
  139. # }}} get_audio_feature_data #
  140. # get_genre_data {{{ #
  141. def get_genre_data(request, user_secret):
  142. """Return genre data needed to create the graph
  143. TODO
  144. """
  145. user = User.objects.get(secret=user_secret)
  146. genre_counts = (Track.objects.filter(users__exact=user)
  147. .values('genre')
  148. .order_by('genre')
  149. # annotates each genre and not each Track, due to the earlier values() call
  150. .annotate(num_songs=Count('genre'))
  151. )
  152. # genre_counts is a QuerySet with the format
  153. # [{'genre': 'classical', 'num_songs': 100}, {'genre': 'pop', 'num_songs': 50}...]
  154. for genre_dict in genre_counts:
  155. genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'])
  156. '''
  157. Now genre_counts has the format
  158. [
  159. {'genre': 'classical',
  160. 'num_songs': 100,
  161. 'artists': {
  162. 'Helene Grimaud': 40.5,
  163. 'Beethoven': 31.2,
  164. 'Mozart': 22...
  165. }
  166. },
  167. {'genre': 'pop',
  168. 'num_songs': 150,
  169. 'artists': {...}
  170. },...
  171. ]
  172. '''
  173. if CONSOLE_LOGGING:
  174. print("*** Genre Breakdown ***")
  175. pprint(list(genre_counts))
  176. return JsonResponse(data=list(genre_counts), safe=False)
  177. # }}} get_genre_data #
  178. # import_history {{{ #
  179. def import_history(request, upload_id):
  180. """Import history for the user from the file they uploaded.
  181. :upload_id: ID (PK) of the HistoryUpload entry
  182. :returns: None
  183. """
  184. # setup {{{ #
  185. headers = ['timestamp', 'track_id']
  186. upload_obj = HistoryUpload.objects.get(id=upload_id)
  187. user_headers = get_user_header(upload_obj.user)
  188. with upload_obj.document.open('r') as f:
  189. csv_reader = csv.reader(f, delimiter=',')
  190. rows_read = 0
  191. history_obj_info_lst = []
  192. artist_genre_queue = []
  193. # skip header row
  194. last_row, history_obj_info = get_next_history_row(csv_reader, headers,
  195. {})
  196. while not last_row:
  197. last_row, history_obj_info = get_next_history_row(csv_reader,
  198. headers, history_obj_info)
  199. # }}} setup #
  200. history_obj_info_lst.append(history_obj_info)
  201. # PU: refactor saving History object right away if Track obj already
  202. # exists
  203. # PU: refactor below?
  204. rows_read += 1
  205. if (rows_read % TRACKS_LIMIT == 0) or last_row:
  206. # get tracks_response {{{ #
  207. track_ids_lst = [info['track_id'] for info in history_obj_info_lst]
  208. # print(len(track_ids_lst))
  209. track_ids = ','.join(track_ids_lst)
  210. payload = {'ids': track_ids}
  211. tracks_response = requests.get(TRACKS_ENDPOINT,
  212. headers=user_headers,
  213. params=payload).json()['tracks']
  214. responses_processed = 0
  215. # }}} get tracks_response #
  216. for track_dict in tracks_response:
  217. # don't associate history track with User, not necessarily in their
  218. # library
  219. track_artists = save_track_artists(track_dict, artist_genre_queue,
  220. user_headers)
  221. track_obj, track_created = save_track_obj(track_dict,
  222. track_artists, None)
  223. timestamp = \
  224. parse(history_obj_info_lst[responses_processed]['timestamp'])
  225. history_obj = save_history_obj(upload_obj.user, timestamp,
  226. track_obj)
  227. if CONSOLE_LOGGING:
  228. print("Processed row #{}: {}".format(
  229. (rows_read - TRACKS_LIMIT) + responses_processed, history_obj,))
  230. responses_processed += 1
  231. history_obj_info_lst = []
  232. if len(artist_genre_queue) > 0:
  233. add_artist_genres(user_headers, artist_genre_queue)
  234. # TODO: update track genres from History relation
  235. # update_track_genres(user_obj)
  236. return redirect('graphs:display_history_table')
  237. # }}} get_history #