def post(self, request, *args, **kwargs): # Reject request if user is ratelimited if request.limited: logger.warning( 'User {} has been rate limited from suggesting songs'.format( request.user.username), extra={ 'fingerprint': auto_fingerprint('rate_limit_suggest_song', **kwargs), 'user_id': request.user.id, 'trace_id': request.trace_id, }) messages.error( request, 'You have submitted too many suggestions! Try again in a minute' ) return HttpResponseRedirect(reverse('spotify:suggest')) form = self.form_class(request.POST) if form.is_valid(): code = form.cleaned_data['code'] FetchSongFromSpotifyTask().delay(code, username=request.user.username, trace_id=request.trace_id) logger.info( 'Called task to add suggestion for song {} by user {}'.format( code, request.user.username), extra={ 'fingerprint': auto_fingerprint('added_suggested_song', **kwargs), 'trace_id': request.trace_id, }) messages.info( request, 'Your song has been slated to be added! Keep an eye out for it in the future' ) return HttpResponseRedirect(reverse('spotify:suggest')) else: logger.warning('User {} suggested an invalid song code: {}'.format( request.user.username, request.POST.get('code'), ), extra={ 'fingerprint': auto_fingerprint('invalid_suggested_song', **kwargs), 'trace_id': request.trace_id, 'errors': form.errors, }) return render(request, self.template_name, context={'form': form})
def create(self, request, *args, **kwargs): try: song = Song.objects.get(code=self.cleaned_data['song_code']) except (Song.DoesNotExist, Song.MultipleObjectsReturned): logger.warning( 'Unable to retrieve song with code {}'.format(self.cleaned_data['song_code']), extra={ 'fingerprint': auto_fingerprint('song_not_found', **kwargs), 'trace_id': request.trace_id, } ) raise Http404('No song exists with code: {}'.format(self.cleaned_data['song_code'])) emotion = Emotion.objects.get(name=self.cleaned_data['emotion']) vote_data = { 'user_id': self.request.user.id, 'emotion_id': emotion.id, 'song_id': song.id, 'vote': self.cleaned_data['vote'], 'context': self.cleaned_data.get('context', ''), 'description': self.cleaned_data.get('description', '') } try: vote = UserSongVote(**vote_data) vote._trace_id = request.trace_id vote.save() logger.info( 'Saved vote for user {} voting on song {} for emotion {}'.format( self.request.user.username, song.code, emotion.full_name ), extra={ 'vote_data': vote_data, 'emotion': emotion.full_name, 'vote_id': vote.pk, 'fingerprint': auto_fingerprint('created_new_vote', **kwargs), 'trace_id': request.trace_id, } ) return JsonResponse({'status': 'OK'}, status=status.HTTP_201_CREATED) except IntegrityError as exc: logger.warning( 'Bad data supplied to VoteView.create from {}'.format(self.request.user.username), extra={ 'vote_data': vote_data, 'fingerprint': auto_fingerprint('bad_vote_data', **kwargs), 'exception_info': exc, 'trace_id': request.trace_id, } ) raise ValidationError('Bad data supplied to {}'.format(self.__class__.__name__))
def run(self, auth_id, playlist_name, songs, cover_image_filename=None, *args, **kwargs): self.trace_id = kwargs.get('trace_id', '') auth = SpotifyAuth.get_and_refresh_spotify_auth_record(auth_id, trace_id=self.trace_id) # Check that user has granted proper scopes to export playlist to Spotify if not auth.has_scope(settings.SPOTIFY_PLAYLIST_MODIFY_SCOPE): logger.error( 'User {} has not granted proper scopes to export playlist to Spotify'.format(auth.user.username), extra={ 'fingerprint': auto_fingerprint('missing_scopes_for_playlist_export', **kwargs), 'auth_id': auth.pk, 'scopes': auth.scopes, 'trace_id': self.trace_id, } ) raise InsufficientSpotifyScopesError('Insufficient Spotify scopes to export playlist') spotify = SpotifyClient(identifier='spotify.tasks.ExportSpotifyPlaylistFromSongsTask-{}'.format(self.trace_id)) logger.info( 'Exporting songs to playlist {} for user {} on Spotify'.format(playlist_name, auth.spotify_user_id), extra={ 'fingerprint': auto_fingerprint('start_export_playlist', **kwargs), 'auth_id': auth.pk, 'trace_id': self.trace_id, } ) playlist_id = self.get_or_create_playlist(auth.access_token, auth.spotify_user_id, playlist_name, spotify) # Upload cover image for playlist if specified if auth.has_scope(settings.SPOTIFY_UPLOAD_PLAYLIST_IMAGE) and cover_image_filename: self.upload_cover_image(auth.access_token, playlist_id, cover_image_filename, spotify) self.delete_all_songs_from_playlist(auth.access_token, playlist_id, spotify) self.add_songs_to_playlist(auth.access_token, playlist_id, songs, spotify) # Delete cover image file from disk if present # # Do this after uploading songs to playlist to keep image file on disk # in case of errors with uploading songs to playlist to ensure that if # we need to retry because of errors with adding/deleting songs in playlist # that we still have the image file on disk for retries. if cover_image_filename: try: os.unlink(cover_image_filename) # pragma: no cover except FileNotFoundError: pass logger.info( 'Exported songs to playlist {} for user {} successfully'.format(playlist_name, auth.spotify_user_id), extra={ 'fingerprint': auto_fingerprint('success_export_playlist', **kwargs), 'auth_id': auth.pk, 'trace_id': self.trace_id, } )
def run(self, spotify_code, username='******', *args, **kwargs): """ Use Spotify API to fetch song data for a given song and save the song to the database :param spotify_code: (str) Spotify URI for the song to be created :param username: (str) [Optional] Username for the user that requested this song """ trace_id = kwargs.get('trace_id', '') signature = 'spotify.tasks.FetchSongFromSpotifyTask-{}'.format(trace_id) # Early exit: if song already exists in our system don't do the work to fetch it if Song.objects.filter(code=spotify_code).exists(): logger.info( 'Song with code {} already exists in database'.format(spotify_code), extra={ 'fingerprint': auto_fingerprint('song_already_exists', **kwargs), 'trace_id': trace_id, } ) return client = SpotifyClient(identifier=signature) track_data = client.get_attributes_for_track(spotify_code) song_data = client.get_audio_features_for_tracks([track_data])[0] try: Song.objects.create(**song_data) logger.info( 'Created song {} in database'.format(spotify_code), extra={ 'fingerprint': auto_fingerprint('created_song', **kwargs), 'song_data': song_data, 'username': username, 'trace_id': trace_id, } ) except ValidationError: logger.exception( 'Failed to create song {}, already exists in database'.format(spotify_code), extra={ 'fingerprint': auto_fingerprint('failed_to_create_song', **kwargs), 'trace_id': trace_id, } ) raise
def handle(self, *args, **options): self.write_to_log_and_output( 'Starting run to create songs from Spotify', extra={ 'fingerprint': auto_fingerprint('start_create_songs_from_spotify', **options) }) tracks = self.get_tracks_from_spotify() if not tracks: # If we didn't get any tracks back from Spotify, raise an exception # This will get caught by the periodic task and retry the script again self.write_to_log_and_output( 'Failed to fetch any tracks from Spotify', output_stream='stderr', log_level=logging.WARNING, extra={ 'fingerprint': auto_fingerprint('failed_to_fetch_tracks_from_spotify', **options) }) raise CommandError('Failed to fetch any songs from Spotify') self.write_to_log_and_output( 'Got {} tracks from Spotify'.format(len(tracks)), extra={ 'fingerprint': auto_fingerprint('fetched_tracks_from_spotify', **options), 'fetched_tracks': len(tracks) }) succeeded, failed = self.save_songs_to_database(tracks) self.write_to_log_and_output( 'Finished run to create songs from Spotify', extra={ 'fingerprint': auto_fingerprint('finish_create_songs_from_spotify', **options), 'saved_tracks': succeeded, 'failed_tracks': failed }) return 'Created Songs: {}'.format(succeeded)
def run(self, *args, **kwargs): logger.info('Starting run to backup mission critical database tables', extra={ 'fingerprint': auto_fingerprint('start_database_backup', **kwargs) }) self.delete_old_backups() self.backup_models() logger.info('Finished run to backup mission critical database tables', extra={ 'fingerprint': auto_fingerprint('finished_database_backup', **kwargs) })
def test_happy_path(self): test = Test() kwargs = test.foo() fingerprint = auto_fingerprint('testing', **kwargs) self.assertEqual(fingerprint, 'libs.tests.test_moody_logging.Test.foo.testing')
def get_and_refresh_spotify_auth_record(cls, auth_id, **kwargs): """ Fetch the SpotifyAuth record for the given primary key, and refresh if the access token is expired :param auth_id: (int) Primary key for SpotifyAuth record :return: (SpotifyAuth) """ trace_id = kwargs.get('trace_id', '') try: auth = cls.objects.get(pk=auth_id) except (SpotifyAuth.MultipleObjectsReturned, SpotifyAuth.DoesNotExist): logger.error( 'Failed to fetch SpotifyAuth with pk={}'.format(auth_id), extra={ 'fingerprint': auto_fingerprint('failed_to_fetch_spotify_auth', **kwargs), 'trace_id': trace_id, }, ) raise if auth.should_refresh_access_token: auth.refresh_access_token(trace_id=trace_id) return auth
def get_or_create_playlist(self, auth_code, spotify_user_id, playlist_name, spotify, **kwargs): """ Get the Spotify playlist by name for the user, creating it if it does not exist :param auth_code: (str) SpotifyAuth access_token for the given user :param spotify_user_id: (str) Spotify username for the given user :param playlist_name: (str) Name of the playlist to be created :param spotify: (spotify_client.SpotifyClient) Spotify Client instance :return: (str) """ playlist_id = None try: resp = spotify.get_user_playlists(auth_code, spotify_user_id) playlists = resp['items'] for playlist in playlists: if playlist['name'] == playlist_name: playlist_id = playlist['id'] break except SpotifyException: logger.warning( 'Error getting playlists for user {}'.format(spotify_user_id), exc_info=True, extra={ 'fingerprint': auto_fingerprint('failed_getting_user_playlists', **kwargs), 'spotify_user_id': spotify_user_id, 'trace_id': self.trace_id, } ) if playlist_id is None: playlist_id = spotify.create_playlist(auth_code, spotify_user_id, playlist_name) logger.info( 'Created playlist for user {} with name {} successfully'.format(spotify_user_id, playlist_name), extra={ 'fingerprint': auto_fingerprint('created_spotify_playlist', **kwargs), 'trace_id': self.trace_id, } ) return playlist_id
def destroy(self, request, *args, **kwargs): votes = UserSongVote.objects.filter( user_id=self.request.user.id, emotion__name=self.cleaned_data['emotion'], song__code=self.cleaned_data['song_code'], vote=True ) if self.cleaned_data.get('context'): votes = votes.filter(context=self.cleaned_data['context']) if not votes.exists(): logger.warning( 'Unable to find UserSongVote to delete', extra={ 'request_data': self.cleaned_data, 'fingerprint': auto_fingerprint('unvote_fail_missing_vote', **kwargs), 'trace_id': request.trace_id, } ) raise Http404() for vote in votes: vote._trace_id = request.trace_id vote.delete() logger.info( 'Deleted vote for user {} with song {} and emotion {} and context {}'.format( self.request.user.username, self.cleaned_data['song_code'], Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']), vote.context or 'None', ), extra={ 'fingerprint': auto_fingerprint('unvote_success', **kwargs), 'vote_id': vote.pk, 'data': self.cleaned_data, 'trace_id': request.trace_id, } ) return JsonResponse({'status': 'OK'})
def run(self, *args, **kwargs): auth_records = SpotifyAuth.objects.all() logger.info( 'Starting run to refresh top artists for {} auth records'.format(auth_records.count()), extra={'fingerprint': auto_fingerprint('refresh_top_artists', **kwargs)} ) for auth in auth_records: UpdateTopArtistsFromSpotifyTask().delay(auth.pk)
def run(self, user_id, *args, **kwargs): """ Create UserEmotion records for a user for each emotion we have in our system :param user_id: (int) Primary key for user record """ trace_id = kwargs.get('trace_id', '') try: user = MoodyUser.objects.get(pk=user_id) except (MoodyUser.DoesNotExist, MoodyUser.MultipleObjectsReturned): logger.exception( 'Unable to fetch MoodyUser with pk={}'.format(user_id), extra={ 'fingerprint': auto_fingerprint('failed_to_fetch_user', **kwargs), 'trace_id': trace_id, }) raise user_emotions = [] for emotion in Emotion.objects.all().iterator(): user_emotions.append( UserEmotion( user=user, emotion=emotion, energy=emotion.energy, valence=emotion.valence, danceability=emotion.danceability, )) UserEmotion.objects.bulk_create(user_emotions) logger.info( 'Created UserEmotion records for user {}'.format(user.username), extra={ 'fingerprint': auto_fingerprint('created_user_emotion_records', **kwargs), 'trace_id': trace_id, })
def post(self, request, *args, **kwargs): form = self.form_class(request.POST) if form.is_valid(): user = MoodyUser( username=form.cleaned_data['username'], email=form.cleaned_data.get('email'), ) user.set_password(form.cleaned_data['password']) user._trace_id = request.trace_id user.save() UserProfile.objects.create(user=user) logger.info( 'Created new user: {}'.format(user.username), extra={ 'fingerprint': auto_fingerprint('created_new_user', **kwargs), 'trace_id': request.trace_id, } ) messages.info(request, 'Your account has been created.') return HttpResponseRedirect(settings.LOGIN_URL) else: logger.warning( 'Failed to create new user because of invalid data', extra={ 'errors': form.errors, 'request_data': { 'username': request.POST.get('username'), 'email': request.POST.get('email') }, 'fingerprint': auto_fingerprint('failed_to_create_new_user', **kwargs), 'trace_id': request.trace_id } ) return render(request, self.template_name, {'form': form})
def run(self, auth_id, *args, **kwargs): trace_id = kwargs.get('trace_id', '') auth = SpotifyAuth.get_and_refresh_spotify_auth_record(auth_id, trace_id=trace_id) # Check that user has granted proper scopes to fetch top artists from Spotify if not auth.has_scope(settings.SPOTIFY_TOP_ARTIST_READ_SCOPE): logger.error( 'User {} has not granted proper scopes to fetch top artists from Spotify'.format(auth.user.username), extra={ 'fingerprint': auto_fingerprint('missing_scopes_for_update_top_artists', **kwargs), 'auth_id': auth.pk, 'scopes': auth.scopes, 'trace_id': trace_id, } ) raise InsufficientSpotifyScopesError('Insufficient Spotify scopes to fetch Spotify top artists') spotify_client_identifier = 'update_spotify_top_artists_{}'.format(auth.spotify_user_id) spotify = SpotifyClient(identifier=spotify_client_identifier) logger.info( 'Updating top artists for {}'.format(auth.spotify_user_id), extra={ 'fingerprint': auto_fingerprint('update_spotify_top_artists', **kwargs), 'trace_id': trace_id, } ) artists = spotify.get_user_top_artists(auth.access_token, settings.SPOTIFY['max_top_artists']) spotify_user_data, _ = SpotifyUserData.objects.get_or_create(spotify_auth=auth) spotify_user_data.top_artists = artists spotify_user_data.save() logger.info( 'Successfully updated top artists for {}'.format(auth.spotify_user_id), extra={ 'fingerprint': auto_fingerprint('success_update_spotify_top_artists', **kwargs), 'trace_id': trace_id, } )
def list(self, request, *args, **kwargs): logger.info( 'Generating {} emotion playlist for user {}'.format( self.cleaned_data['emotion'], self.request.user.username, ), extra={ 'fingerprint': auto_fingerprint('generate_emotion_playlist', **kwargs), 'user_id': self.request.user.pk, 'emotion': Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']), 'genre': self.cleaned_data.get('genre'), 'context': self.cleaned_data.get('context'), 'artist': self.cleaned_data.get('artist'), 'page': self.request.GET.get('page'), 'trace_id': request.trace_id, } ) resp = super().list(request, *args, **kwargs) first_page = last_page = None if resp.data['previous']: first_page = re.sub(r'&page=[0-9]*', '', resp.data['previous']) if resp.data['next']: last_page = re.sub(r'page=[0-9]*', 'page=last', resp.data['next']) queryset = self.filter_queryset(self.get_queryset()) # Update response data with analytics for emotion votes_for_emotion_data = average(queryset, 'song__valence', 'song__energy', 'song__danceability') valence = votes_for_emotion_data['song__valence__avg'] or 0 energy = votes_for_emotion_data['song__energy__avg'] or 0 danceability = votes_for_emotion_data['song__danceability__avg'] or 0 # Normalize emotion data as whole numbers normalized_valence = valence * 100 normalized_energy = energy * 100 normalized_danceability = danceability * 100 resp.data.update({ 'valence': normalized_valence, 'energy': normalized_energy, 'danceability': normalized_danceability, 'emotion_name': Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']), 'first_page': first_page, 'last_page': last_page, }) return resp
def delete_old_backups(self, **kwargs): for backup_file in os.listdir(settings.DATABASE_BACKUPS_PATH): if any([ backup_file.startswith(model) for model in settings.DATABASE_BACKUP_TARGETS ]): backup_filename = os.path.join(settings.DATABASE_BACKUPS_PATH, backup_file) logger.info('Deleting old backup {}'.format(backup_filename), extra={ 'fingerprint': auto_fingerprint('delete_old_backup', **kwargs) }) os.unlink(backup_filename)
def refresh_access_token(self, **kwargs): """Make a call to the Spotify API to refresh the access token for the SpotifyAuth record""" trace_id = kwargs.get('trace_id', '') spotify_client = SpotifyClient(identifier='refresh-access-token:{}'.format(self.spotify_user_id)) try: access_token = spotify_client.refresh_access_token(self.refresh_token) self.access_token = access_token self.last_refreshed = timezone.now() self.save() logger.info( 'Refreshed access token for {}'.format(self.spotify_user_id), extra={ 'fingerprint': auto_fingerprint('success_refresh_access_token', **kwargs), 'spotify_username': self.spotify_user_id, 'auth_id': self.pk, 'user_id': self.user_id, 'trace_id': trace_id, } ) except SpotifyException: logger.exception( 'Unable to refresh access token for {}'.format(self.spotify_user_id), extra={ 'fingerprint': auto_fingerprint('failed_refresh_access_token', **kwargs), 'spotify_username': self.spotify_user_id, 'auth_id': self.pk, 'user_id': self.user_id, 'trace_id': trace_id, } ) raise
def backup_models(self, **kwargs): for model in settings.DATABASE_BACKUP_TARGETS: backup_filename = '{backup_directory}/{model_name}.json'.format( backup_directory=settings.DATABASE_BACKUPS_PATH, model_name=model) logger.info('Writing backup of {} to file {}'.format( model, backup_filename), extra={ 'fingerprint': auto_fingerprint('backup_database_model', **kwargs), 'model': model, }) call_command('dumpdata', model, output=backup_filename)
def update(self, request, *args, **kwargs): user_profile = get_object_or_404(UserProfile, user=request.user) for name, value in self.cleaned_data.items(): setattr(user_profile, name, value) user_profile.save() logger.info( 'Updated UserProfile record for user {}'.format(request.user.username), extra={ 'fingerprint': auto_fingerprint('updated_user_profile', **kwargs), 'request_data': self.cleaned_data, 'trace_id': request.trace_id } ) return JsonResponse({'status': 'OK'})
def post(self, request, *args, **kwargs): auth = request.spotify_auth auth_id = auth.pk auth.delete() logger.info('Deleted SpotifyAuth for user {}'.format( request.user.username), extra={ 'fingerprint': auto_fingerprint('revoked_spotify_auth', **kwargs), 'auth_id': auth_id, 'trace_id': request.trace_id, }) messages.info(request, 'We have deleted your Spotify data from Moodytunes') return HttpResponseRedirect(reverse('accounts:profile'))
def get_user_emotion_record(self, emotion_name, **kwargs): """ Return the UserEmotion record for a given Emotion name. :param emotion_name: (str) `Emotion.name` constant to retrieve :return: (UserEmotion) """ try: return self.useremotion_set.get(emotion__name=emotion_name) except UserEmotion.DoesNotExist: logger.warning('User {} has no UserEmotion record for {}'.format( self.username, emotion_name), extra={ 'fingerprint': auto_fingerprint('user_emotion_not_found', **kwargs) }) return None
def upload_cover_image(self, auth_code, playlist_id, cover_image_filename, spotify, **kwargs): """ Upload custom cover image for playlist. If any errors were encountered it will just fail silently. :param auth_code: (str) SpotifyAuth access_token for the given user :param playlist_id: (str) Spotify ID of the playlist to be created :param cover_image_filename: (str) Filename of cover image as a file on disk :param spotify: (spotify_client.SpotifyClient) Spotify Client instance """ try: spotify.upload_image_to_playlist(auth_code, playlist_id, cover_image_filename) except (SpotifyException, ClientException): logger.warning( 'Unable to upload cover image for playlist {}'.format(playlist_id), extra={ 'fingerprint': auto_fingerprint('failed_upload_cover_image', **kwargs), 'trace_id': self.trace_id, }, exc_info=True )
def _log_bad_request(self, request, serializer, **kwargs): """ Helper method to log information about a bad request to our system. :param request: (rest_framework.request.Request) Request object that had failed validation :param serializer: (rest_framework.serializers.Serializer) Serializer object that rejected the request """ # Filter HTTP headers from request metadata request_headers = request.META http_headers = dict([(header, value) for header, value in request_headers.items() if header.startswith('HTTP')]) cleaned_headers = self._clean_headers(copy.deepcopy(http_headers)) request_data = { 'params': request.GET, 'data': request.data, 'user_id': request.user.id, 'headers': cleaned_headers, 'method': request.method, 'errors': serializer.errors, 'view': '{}.{}'.format(self.__class__.__module__, self.__class__.__name__), 'fingerprint': auto_fingerprint('bad_request', **kwargs), 'trace_id': request.trace_id } logger.warning('Invalid {} data supplied to {}'.format( request.method, self.__class__.__name__), extra=request_data)
def post(self, request, *args, **kwargs): form = self.form_class(request.POST, user=request.user) if form.is_valid(): request.user.update_information(form.cleaned_data) messages.info(request, 'Your account information has been updated.') return HttpResponseRedirect(reverse('accounts:profile')) else: logger.warning( 'Failed to update user info because of invalid data', extra={ 'errors': form.errors, 'request_data': { 'username': request.POST.get('username'), 'email': request.POST.get('email') }, 'user_id': request.user.id, 'fingerprint': auto_fingerprint('failed_to_update_user_info', **kwargs), 'trace_id': request.trace_id, } ) return render(request, self.template_name, {'form': form})
def get_object(self, **kwargs): cached_playlist_manager = CachedPlaylistManager(self.request.user) cached_playlist = cached_playlist_manager.retrieve_cached_browse_playlist() if cached_playlist: emotion = cached_playlist['emotion'] playlist = cached_playlist['playlist'] context = cached_playlist.get('context') description = cached_playlist.get('description') # Filter out songs user has already voted on from the playlist # for the emotion to prevent double votes on songs user_voted_songs = self.request.user.usersongvote_set.filter( emotion__name=emotion ).values_list( 'song__pk', flat=True ) playlist = [song for song in playlist if song.pk not in user_voted_songs] return { 'emotion': emotion, 'context': context, 'description': description, 'playlist': playlist, 'trace_id': self.request.trace_id, } else: logger.warning( 'No cached browse playlist found for user {}'.format(self.request.user.username), extra={ 'fingerprint': auto_fingerprint('no_cached_browse_playlist_found', **kwargs), 'trace_id': self.request.trace_id, } ) raise Http404('No cached browse playlist found')
def get(self, request, *args, **kwargs): auth = request.spotify_auth # Check that user has the proper scopes from Spotify to create playlist for scope in settings.SPOTIFY['auth_user_scopes']: if not auth.has_scope(scope): logger.info( 'User {} does not have proper scopes for playlist export. Redirecting to grant scopes' .format(request.user.username), extra={ 'user_id': request.user.pk, 'auth_id': auth.pk, 'scopes': auth.scopes, 'expected_scopes': settings.SPOTIFY['auth_user_scopes'], 'fingerprint': auto_fingerprint('missing_scopes_for_playlist_export', **kwargs), 'trace_id': request.trace_id, }) auth.delete( ) # Delete SpotifyAuth record to ensure that it can be created with proper scopes messages.info( request, 'Please reauthenticate with Spotify to export your playlist' ) return HttpResponseRedirect(reverse('spotify:spotify-auth')) return super().get(request, *args, **kwargs)
def get_tracks_from_spotify(self, **kwargs): """ Request, format, and return tracks from Spotify's API. :return: (list(dict)) Track data for saving as Song records """ spotify = SpotifyClient( identifier='create_songs_from_spotify-{}'.format(self._unique_id)) tracks = [] for category in settings.SPOTIFY['categories']: songs_from_category = 0 try: playlists = spotify.get_playlists_for_category( category, settings.SPOTIFY['max_playlist_from_category']) self.logger.info('Got {} playlists for category: {}'.format( len(playlists), category), extra={ 'fingerprint': auto_fingerprint( 'retrieved_playlists_for_category', **kwargs), 'command_id': self._unique_id }) for playlist in playlists: if songs_from_category < settings.SPOTIFY[ 'max_songs_from_category']: num_tracks = settings.SPOTIFY[ 'max_songs_from_category'] - songs_from_category self.logger.info( 'Calling Spotify API to get {} track(s) for playlist {}' .format(num_tracks, playlist['name']), extra={ 'fingerprint': auto_fingerprint('get_tracks_from_playlist', **kwargs), 'tracks_to_retrieve': num_tracks, 'command_id': self._unique_id }) raw_tracks = spotify.get_songs_from_playlist( playlist, num_tracks) self.logger.info( 'Calling Spotify API to get feature data for {} tracks' .format(len(raw_tracks)), extra={ 'fingerprint': auto_fingerprint('get_feature_data_for_tracks', **kwargs), 'command_id': self._unique_id }) complete_tracks = spotify.get_audio_features_for_tracks( raw_tracks) # Add genre information to each track. We can use the category search term as the genre # for songs found for that category for track in complete_tracks: track.update({'genre': category}) self.logger.info( 'Got {} tracks from {}'.format( len(complete_tracks), playlist['name']), extra={ 'fingerprint': auto_fingerprint( 'retrieved_tracks_from_playlist', **kwargs), 'command_id': self._unique_id }) tracks.extend(complete_tracks) songs_from_category += len(complete_tracks) self.write_to_log_and_output( 'Finished processing {} tracks for category: {}'.format( songs_from_category, category), extra={ 'fingerprint': auto_fingerprint('processed_tracks_for_category', **kwargs) }) except SpotifyException as exc: self.write_to_log_and_output( 'Error connecting to Spotify! Exception detail: {}. ' 'Got {} track(s) successfully.'.format(exc, len(tracks)), output_stream='stderr', log_level=logging.ERROR, extra={ 'fingerprint': auto_fingerprint('caught_spotify_exception', **kwargs) }, exc_info=True, ) break except Exception as exc: self.write_to_log_and_output( 'Unhandled exception when collecting songs from Spotify! Exception detail: {}. ' 'Got {} track(s) successfully.'.format(exc, len(tracks)), output_stream='stderr', log_level=logging.ERROR, extra={ 'fingerprint': auto_fingerprint('caught_unhandled_exception', **kwargs) }, exc_info=True, ) break return tracks
def get(self, request, *args, **kwargs): # Check to make sure that the user who initiated the request is the one making the request # state value for session is set in initial authentication request if request.GET.get('state') != request.session['state']: logger.error( 'User {} has an invalid state parameter for OAuth callback'. format(request.user.username), extra={ 'session_state': request.session.get('state'), 'request_state': request.GET.get('state'), 'fingerprint': auto_fingerprint('invalid_oauth_state', **kwargs), 'trace_id': request.trace_id, }) raise SuspiciousOperation('Invalid state parameter') if 'code' in request.GET: code = request.GET['code'] user = request.user # Early exit: if we already have a SpotifyAuth record for the user, exit if SpotifyAuth.objects.filter(user=user).exists(): messages.info(request, 'You have already authenticated with Spotify!') return HttpResponseRedirect( reverse('spotify:spotify-auth-success')) # Get access and refresh tokens for user spotify_client = SpotifyClient( identifier='spotify.views.SpotifyAuthenticationCallbackView-{}' .format(request.trace_id)) try: tokens = spotify_client.get_access_and_refresh_tokens( code, settings.SPOTIFY['auth_redirect_uri']) except SpotifyException: logger.exception( 'Unable to get Spotify tokens for user {}'.format( user.username), extra={ 'fingerprint': auto_fingerprint('failed_get_spotify_tokens', **kwargs), 'trace_id': request.trace_id, }) messages.error( request, 'We were unable to retrieve your Spotify profile. Please try again.' ) return HttpResponseRedirect( reverse('spotify:spotify-auth-failure')) # Get Spotify username from profile data try: profile_data = spotify_client.get_user_profile( tokens['access_token']) except SpotifyException: logger.exception( 'Unable to get Spotify profile for user {}'.format( user.username), extra={ 'fingerprint': auto_fingerprint('failed_get_spotify_profile', **kwargs), 'trace_id': request.trace_id, }) messages.error( request, 'We were unable to retrieve your Spotify profile. Please try again.' ) return HttpResponseRedirect( reverse('spotify:spotify-auth-failure')) # Create SpotifyAuth record from data try: with transaction.atomic(): auth = SpotifyAuth( user=user, access_token=tokens['access_token'], refresh_token=tokens['refresh_token'], spotify_user_id=profile_data['id'], scopes=settings.SPOTIFY['auth_user_scopes'], ) auth._trace_id = request.trace_id auth.save() logger.info( 'Created SpotifyAuth record for user {}'.format( user.username), extra={ 'fingerprint': auto_fingerprint('created_spotify_auth', **kwargs), 'auth_id': auth.pk, 'user_id': user.pk, 'spotify_user_id': profile_data['id'], 'scopes': settings.SPOTIFY['auth_user_scopes'], 'trace_id': request.trace_id, }) messages.info( request, 'You have successfully authorized Moodytunes with Spotify!' ) redirect_url = request.session.get( 'redirect_url') or reverse( 'spotify:spotify-auth-success') return HttpResponseRedirect(redirect_url) except IntegrityError: logger.exception( 'Failed to create SpotifyAuth record for MoodyUser {} with Spotify username {}' .format(user.username, profile_data['id']), extra={ 'fingerprint': auto_fingerprint('failed_to_create_spotify_auth_user', **kwargs), 'trace_id': request.trace_id, }) messages.error( request, 'Spotify user {} has already authorized MoodyTunes.'. format(profile_data['id'])) return HttpResponseRedirect( reverse('spotify:spotify-auth-failure')) else: logger.warning('User {} failed Spotify Oauth confirmation'.format( request.user.username), extra={ 'fingerprint': auto_fingerprint( 'user_rejected_oauth_confirmation', **kwargs), 'spotify_oauth_error': request.GET['error'], 'trace_id': request.trace_id, }) # Map error code to human-friendly display error_messages = { 'access_denied': 'You rejected to link MoodyTunes to Spotify. Please select Accept next time.' } messages.error( request, error_messages.get(request.GET['error'], request.GET['error'])) return HttpResponseRedirect( reverse('spotify:spotify-auth-failure'))
def filter_queryset(self, queryset, **kwargs): cached_playlist_manager = CachedPlaylistManager(self.request.user) jitter = self.cleaned_data.get('jitter') limit = self.cleaned_data.get('limit') or self.default_limit artist = self.cleaned_data.get('artist') energy = None valence = None danceability = None strategy = random.choice(settings.BROWSE_PLAYLIST_STRATEGIES) # Should be able to supply 0 for jitter, so we'll check explicitly for None if jitter is None: jitter = self.default_jitter # Try to use upvotes for this emotion and context to generate attributes for songs to return if self.cleaned_data.get('context'): votes = self.request.user.usersongvote_set.filter( emotion__name=self.cleaned_data['emotion'], context=self.cleaned_data['context'], vote=True ) if votes.exists(): attributes_for_votes = average(votes, 'song__valence', 'song__energy', 'song__danceability') valence = attributes_for_votes['song__valence__avg'] energy = attributes_for_votes['song__energy__avg'] danceability = attributes_for_votes['song__danceability__avg'] # If context not provided or the previous query on upvotes for context did return any votes, # determine attributes from the attributes for the user and emotion if energy is None or valence is None or valence is None: user_emotion = self.request.user.get_user_emotion_record(self.cleaned_data['emotion']) # If the user doesn't have a UserEmotion record for the emotion, fall back to the # default attributes for the emotion if not user_emotion: emotion = Emotion.objects.get(name=self.cleaned_data['emotion']) energy = emotion.energy valence = emotion.valence danceability = emotion.danceability else: energy = user_emotion.energy valence = user_emotion.valence danceability = user_emotion.danceability # Try to fetch top artists for user from Spotify top_artists = None try: spotify_data = SpotifyUserData.objects.get(spotify_auth__user=self.request.user) top_artists = spotify_data.top_artists except SpotifyUserData.DoesNotExist: pass logger.info( 'Generating {} browse playlist for user {}'.format( self.cleaned_data['emotion'], self.request.user.username, ), extra={ 'fingerprint': auto_fingerprint('generate_browse_playlist', **kwargs), 'user_id': self.request.user.pk, 'emotion': Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']), 'genre': self.cleaned_data.get('genre'), 'context': self.cleaned_data.get('context'), 'strategy': strategy, 'energy': energy, 'valence': valence, 'danceability': danceability, 'artist': artist, 'jitter': jitter, 'top_artists': top_artists, 'trace_id': self.request.trace_id, } ) playlist = generate_browse_playlist( energy, valence, danceability, strategy=strategy, limit=limit, jitter=jitter, artist=artist, top_artists=top_artists, songs=queryset ) cached_playlist_manager.cache_browse_playlist( playlist, self.cleaned_data['emotion'], self.cleaned_data.get('context'), self.cleaned_data.get('description') ) return playlist
def post(self, request, *args, **kwargs): form = self.form_class(request.POST, request.FILES) if form.is_valid(): auth = request.spotify_auth playlist_name = form.cleaned_data['playlist_name'] emotion_name = form.cleaned_data['emotion'] genre = form.cleaned_data['genre'] context = form.cleaned_data['context'] songs = ExportPlaylistHelper.get_export_playlist_for_user( request.user, emotion_name, genre, context) if not songs: msg = 'Your {} playlist is empty! Try adding some songs to export the playlist'.format( Emotion.get_full_name_from_keyword(emotion_name).lower()) messages.error(request, msg) return HttpResponseRedirect(reverse('spotify:export')) # Handle cover image upload cover_image_filename = None if form.cleaned_data.get('cover_image'): cover_image_filename = '{}/{}_{}_{}.jpg'.format( settings.IMAGE_FILE_UPLOAD_PATH, request.user.username, form.cleaned_data['emotion'], form.cleaned_data['playlist_name'], ) img = Image.open(form.cleaned_data['cover_image']) img = img.convert('RGB') with open(cover_image_filename, 'wb+') as img_file: img.save(img_file, format='JPEG') logger.info('Exporting {} playlist for user {} to Spotify'.format( emotion_name, request.user.username), extra={ 'emotion': Emotion.get_full_name_from_keyword(emotion_name), 'genre': genre, 'context': context, 'user_id': request.user.pk, 'auth_id': auth.pk, 'fingerprint': auto_fingerprint('export_playlist_to_spotify', **kwargs), 'trace_id': request.trace_id, }) ExportSpotifyPlaylistFromSongsTask().delay( auth.id, playlist_name, songs, cover_image_filename, trace_id=request.trace_id) messages.info( request, 'Your playlist has been exported! Check in on Spotify in a little bit to see it' ) return HttpResponseRedirect(reverse('spotify:export')) else: messages.error(request, 'Please submit a valid request') return render(request, self.template_name, {'form': form})