def test_insert_user_stats_mult_ranges_artist(self): """ Test if multiple time range data is inserted correctly """ with open(self.path_to_data_file('user_top_artists_db.json')) as f: artists_data = json.load(f) db_stats.insert_user_artists(user_id=self.user['id'], artists=UserArtistStatJson(**{'all_time': artists_data})) db_stats.insert_user_artists(user_id=self.user['id'], artists=UserArtistStatJson(**{'year': artists_data})) result = db_stats.get_user_artists(1, 'all_time') self.assertDictEqual(result.all_time.dict(), artists_data) result = db_stats.get_user_artists(1, 'year') self.assertDictEqual(result.year.dict(), artists_data)
def artists(user_name): """ Show the top artists for the user. These users must have been already calculated using Google BigQuery. If the stats are not present, we redirect to the user page with a message. """ try: user = _get_user(user_name) data = db_stats.get_user_artists(user.id) except DatabaseException as e: current_app.logger.error( 'Error while getting top artist page for user %s: %s', user.musicbrainz_id, str(e)) raise # if no data, flash a message and return to profile page if data is None: msg = ( 'No data calculated for user %s yet. ListenBrainz only calculates statistics for' ' recently active users. If %s has logged in recently, they\'ve already been added to' ' the stats calculation queue. Please wait until the next statistics calculation batch is finished' ' or request stats calculation from your info page.') % (user_name, user_name) flash.error(msg) return redirect(url_for('user.profile', user_name=user_name)) top_artists = data['artist']['all_time'] return render_template("user/artists.html", user=user, data=ujson.dumps(top_artists), section='artists')
def artists(user_name): """ Show the top artists for the user. These users must have been already calculated using Google BigQuery. If the stats are not present, we redirect to the user page with a message. """ try: user = _get_user(user_name) data = db_stats.get_user_artists(user.id) except DatabaseException as e: current_app.logger.error('Error while getting top artist page for user %s: %s', user.musicbrainz_id, str(e)) raise # if no data, flash a message and return to profile page if data is None: msg = ('No data calculated for user %s yet. ListenBrainz only calculates statistics for' ' recently active users. If %s has logged in recently, they\'ve already been added to' ' the stats calculation queue. Please wait until the next statistics calculation batch is finished' ' or request stats calculation from your info page.') % (user_name, user_name) flash.error(msg) return redirect(url_for('user.profile', user_name=user_name)) top_artists = data['artist']['all_time'] return render_template( "user/artists.html", user=user, data=ujson.dumps(top_artists), section='artists' )
def test_insert_user_artists(self): """ Test if artist stats are inserted correctly """ with open(self.path_to_data_file('user_top_artists_db.json')) as f: artists_data = json.load(f) db_stats.insert_user_artists(user_id=self.user['id'], artists=UserArtistStatJson(**{'all_time': artists_data})) result = db_stats.get_user_artists(user_id=self.user['id'], stats_range='all_time') self.assertDictEqual(result.all_time.dict(), artists_data)
def test_delete(self): user_id = db_user.create(10, 'frank') user = db_user.get(user_id) self.assertIsNotNone(user) with open(self.path_to_data_file('user_top_artists_db.json')) as f: artists_data = ujson.load(f) db_stats.insert_user_artists( user_id=user_id, artists=UserArtistStatJson(**{'all_time': artists_data}), ) user_stats = db_stats.get_user_artists(user_id, 'all_time') self.assertIsNotNone(user_stats) db_user.delete(user_id) user = db_user.get(user_id) self.assertIsNone(user) user_stats = db_stats.get_user_artists(user_id, 'all_time') self.assertIsNone(user_stats)
def profile(user_name): # Which database to use to showing user listens. db_conn = webserver.timescale_connection._ts # Which database to use to show playing_now stream. playing_now_conn = webserver.redis_connection._redis user = _get_user(user_name) # User name used to get user may not have the same case as original user name. user_name = user.musicbrainz_id # Getting data for current page max_ts = request.args.get("max_ts") if max_ts is not None: try: max_ts = int(max_ts) except ValueError: raise BadRequest("Incorrect timestamp argument max_ts: %s" % request.args.get("max_ts")) min_ts = request.args.get("min_ts") if min_ts is not None: try: min_ts = int(min_ts) except ValueError: raise BadRequest("Incorrect timestamp argument min_ts: %s" % request.args.get("min_ts")) # Send min and max listen times to allow React component to hide prev/next buttons accordingly (min_ts_per_user, max_ts_per_user) = db_conn.get_timestamps_for_user(user_name) if max_ts is None and min_ts is None: if max_ts_per_user: max_ts = max_ts_per_user + 1 else: max_ts = int(time.time()) listens = [] if min_ts_per_user != max_ts_per_user: args = {} if max_ts: args['to_ts'] = max_ts else: args['from_ts'] = min_ts for listen in db_conn.fetch_listens(user_name, limit=LISTENS_PER_PAGE, **args): listens.append({ "track_metadata": listen.data, "listened_at": listen.ts_since_epoch, "listened_at_iso": listen.timestamp.isoformat() + "Z", }) # If there are no previous listens then display now_playing if not listens or listens[0]['listened_at'] >= max_ts_per_user: playing_now = playing_now_conn.get_playing_now(user.id) if playing_now: listen = { "track_metadata": playing_now.data, "playing_now": "true", } listens.insert(0, listen) user_stats = db_stats.get_user_artists(user.id, 'all_time') try: artist_count = user_stats.all_time.count except (AttributeError, ValidationError): artist_count = None spotify_data = {} current_user_data = {} logged_in_user_follows_user = None if current_user.is_authenticated: spotify_data = spotify.get_user_dict(current_user.id) current_user_data = { "id": current_user.id, "name": current_user.musicbrainz_id, "auth_token": current_user.auth_token, } logged_in_user_follows_user = db_user_relationship.is_following_user(current_user.id, user.id) props = { "user": { "id": user.id, "name": user.musicbrainz_id, }, "current_user": current_user_data, "listens": listens, "latest_listen_ts": max_ts_per_user, "oldest_listen_ts": min_ts_per_user, "latest_spotify_uri": _get_spotify_uri_for_listens(listens), "artist_count": format(artist_count, ",d") if artist_count else None, "profile_url": url_for('user.profile', user_name=user_name), "mode": "listens", "spotify": spotify_data, "web_sockets_server_url": current_app.config['WEBSOCKETS_SERVER_URL'], "api_url": current_app.config['API_URL'], "logged_in_user_follows_user": logged_in_user_follows_user, } return render_template("user/profile.html", props=ujson.dumps(props), mode='listens', user=user, active_section='listens')
def get_artist_map(user_name: str): """ Get the artist map for user ``user_name``. The artist map shows the number of artists the user has listened to from different countries of the world. A sample response from the endpoint may look like:: { "payload": { "from_ts": 1587945600, "last_updated": 1592807084, "artist_map": [ { "country": "USA", "artist_count": 34 }, { "country": "GBR", "artist_count": 69 }, { "country": "IND", "artist_count": 32 } ], "stats_range": "all_time" "to_ts": 1589155200, "user_id": "ishaanshah" } } .. note:: - This endpoint is currently in beta - We cache the results for this query for a week to improve page load times, if you want to request fresh data you can use the ``force_recalculate`` flag. :param range: Optional, time interval for which statistics should be returned, possible values are ``week``, ``month``, ``year``, ``all_time``, defaults to ``all_time`` :type range: ``str`` :param force_recalculate: Optional, recalculate the data instead of returning the cached result. :type range: ``bool`` :statuscode 200: Successful query, you have data! :statuscode 204: Statistics for the user haven't been calculated, empty response will be returned :statuscode 400: Bad request, check ``response['error']`` for more details :statuscode 404: User not found :resheader Content-Type: *application/json* """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: {}".format(user_name)) stats_range = request.args.get('range', default='all_time') if not _is_valid_range(stats_range): raise APIBadRequest("Invalid range: {}".format(stats_range)) recalculate_param = request.args.get('force_recalculate', default='false') if recalculate_param.lower() not in ['true', 'false']: raise APIBadRequest( "Invalid value of force_recalculate: {}".format(recalculate_param)) force_recalculate = recalculate_param.lower() == 'true' # Check if stats are present in DB, if not calculate them calculated = not force_recalculate stats = db_stats.get_user_artist_map(user['id'], stats_range) if stats is None or getattr(stats, stats_range) is None: calculated = False # Check if the stats present in DB have been calculated in the past week, if not recalculate them stale = False if calculated: last_updated = getattr(stats, stats_range).last_updated if (datetime.now() - datetime.fromtimestamp(last_updated) ).days >= STATS_CALCULATION_INTERVAL: stale = True if stale or not calculated: artist_stats = db_stats.get_user_artists(user['id'], stats_range) # If top artists are missing, return the stale stats if present, otherwise return 204 if artist_stats is None or getattr(artist_stats, stats_range) is None: if stale: result = stats else: raise APINoContent('') else: # Calculate the data artist_msids = defaultdict(lambda: 0) artist_mbids = defaultdict(lambda: 0) top_artists = getattr(artist_stats, stats_range).artists for artist in top_artists: if artist.artist_msid is not None: artist_msids[artist.artist_msid] += artist.listen_count else: for artist_mbid in artist.artist_mbids: artist_mbids[artist_mbid] += artist.listen_count country_code_data = _get_country_codes(artist_msids, artist_mbids) result = UserArtistMapStatJson( **{ stats_range: { "artist_map": country_code_data, "from_ts": int( getattr(artist_stats, stats_range).from_ts), "to_ts": int(getattr(artist_stats, stats_range).to_ts), "last_updated": int(datetime.now().timestamp()) } }) # Store in DB for future use try: db_stats.insert_user_artist_map(user['id'], result) except Exception as err: current_app.logger.error( "Error while inserting artist map stats for {user}. Error: {err}. Data: {data}" .format(user=user_name, err=err, data=result), exc_info=True) else: result = stats return jsonify({ "payload": { "user_id": user_name, "range": stats_range, **(getattr(result, stats_range).dict()) } })
def get_user_artist(user_name): """ Get top artists for user ``user_name``. A sample response from the endpoint may look like:: { "payload": { "artists": [ { "artist_mbids": ["93e6118e-7fa8-49f6-9e02-699a1ebce105"], "artist_msid": "d340853d-7408-4a0d-89c2-6ff13e568815", "artist_name": "The Local train", "listen_count": 385 }, { "artist_mbids": ["ae9ed5e2-4caf-4b3d-9cb3-2ad626b91714"], "artist_msid": "ba64b195-01dd-4613-9534-bb87dc44cffb", "artist_name": "Lenka", "listen_count": 333 }, { "artist_mbids": ["cc197bad-dc9c-440d-a5b5-d52ba2e14234"], "artist_msid": "6599e41e-390c-4855-a2ac-68ee798538b4", "artist_name": "Coldplay", "listen_count": 321 } ], "count": 3, "total_artist_count": 175, "range": "all_time", "last_updated": 1588494361, "user_id": "John Doe", "from_ts": 1009823400, "to_ts": 1590029157 } } .. note:: - This endpoint is currently in beta - ``artist_mbids`` and ``artist_msid`` are optional fields and may not be present in all the responses :param count: Optional, number of artists to return, Default: :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` Max: :data:`~webserver.views.api.MAX_ITEMS_PER_GET` :type count: ``int`` :param offset: Optional, number of artists to skip from the beginning, for pagination. Ex. An offset of 5 means the top 5 artists will be skipped, defaults to 0 :type offset: ``int`` :param range: Optional, time interval for which statistics should be collected, possible values are ``week``, ``month``, ``year``, ``all_time``, defaults to ``all_time`` :type range: ``str`` :statuscode 200: Successful query, you have data! :statuscode 204: Statistics for the user haven't been calculated, empty response will be returned :statuscode 400: Bad request, check ``response['error']`` for more details :statuscode 404: User not found :resheader Content-Type: *application/json* """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: %s" % user_name) stats_range = request.args.get('range', default='all_time') if not _is_valid_range(stats_range): raise APIBadRequest("Invalid range: {}".format(stats_range)) offset = get_non_negative_param('offset', default=0) count = get_non_negative_param('count', default=DEFAULT_ITEMS_PER_GET) stats = db_stats.get_user_artists(user['id'], stats_range) if stats is None or getattr(stats, stats_range) is None: raise APINoContent('') entity_list, total_entity_count = _process_user_entity(stats, stats_range, offset, count, entity='artist') from_ts = int(getattr(stats, stats_range).from_ts) to_ts = int(getattr(stats, stats_range).to_ts) last_updated = int(stats.last_updated.timestamp()) return jsonify({ 'payload': { "user_id": user_name, "artists": entity_list, "count": len(entity_list), "total_artist_count": total_entity_count, "offset": offset, "range": stats_range, "from_ts": from_ts, "to_ts": to_ts, "last_updated": last_updated, } })
def test_get_user_artists(self): data_inserted = self.insert_test_data() data = db_stats.get_user_artists(self.user['id']) self.assertEqual(data['artist']['count'], 2)
def test_get_user_artists(self): data_inserted = self.insert_test_data() result = db_stats.get_user_artists(self.user['id'], 'all_time') self.assertDictEqual(result.all_time.dict(), data_inserted['user_artists'])
def profile(user_name): # Which database to use to showing user listens. db_conn = webserver.influx_connection._influx # Which database to use to show playing_now stream. playing_now_conn = webserver.redis_connection._redis user = _get_user(user_name) # User name used to get user may not have the same case as original user name. user_name = user.musicbrainz_id try: have_listen_count = True listen_count = db_conn.get_listen_count_for_user(user_name) except (InfluxDBServerError, InfluxDBClientError): have_listen_count = False listen_count = 0 # Getting data for current page max_ts = request.args.get("max_ts") if max_ts is not None: try: max_ts = int(max_ts) except ValueError: raise BadRequest("Incorrect timestamp argument max_ts: %s" % request.args.get("max_ts")) min_ts = request.args.get("min_ts") if min_ts is not None: try: min_ts = int(min_ts) except ValueError: raise BadRequest("Incorrect timestamp argument min_ts: %s" % request.args.get("min_ts")) if max_ts is None and min_ts is None: max_ts = int(time.time()) args = {} if max_ts: args['to_ts'] = max_ts else: args['from_ts'] = min_ts listens = [] for listen in db_conn.fetch_listens(user_name, limit=LISTENS_PER_PAGE, **args): # Let's fetch one more listen, so we know to show a next page link or not listens.append({ "track_metadata": listen.data, "listened_at": listen.ts_since_epoch, "listened_at_iso": listen.timestamp.isoformat() + "Z", }) # Calculate if we need to show next/prev buttons previous_listen_ts = None next_listen_ts = None if listens: (min_ts_per_user, max_ts_per_user) = db_conn.get_timestamps_for_user(user_name) if min_ts_per_user >= 0: if listens[-1]['listened_at'] > min_ts_per_user: next_listen_ts = listens[-1]['listened_at'] else: next_listen_ts = None if listens[0]['listened_at'] < max_ts_per_user: previous_listen_ts = listens[0]['listened_at'] else: previous_listen_ts = None # If there are no previous listens then display now_playing if not previous_listen_ts: playing_now = playing_now_conn.get_playing_now(user.id) if playing_now: listen = { "track_metadata": playing_now.data, "playing_now": "true", } listens.insert(0, listen) user_stats = db_stats.get_user_artists(user.id) try: artist_count = int(user_stats['artist']['count']) except (KeyError, TypeError): artist_count = None return render_template( "user/profile.html", user=user, listens=listens, previous_listen_ts=previous_listen_ts, next_listen_ts=next_listen_ts, spotify_uri=_get_spotify_uri_for_listens(listens), have_listen_count=have_listen_count, listen_count=format(int(listen_count), ",d"), artist_count=format(artist_count, ",d") if artist_count else None, section='listens')
def profile(user_name): # Which database to use to showing user listens. db_conn = webserver.influx_connection._influx # Which database to use to show playing_now stream. playing_now_conn = webserver.redis_connection._redis user = _get_user(user_name) # User name used to get user may not have the same case as original user name. user_name = user.musicbrainz_id try: have_listen_count = True listen_count = db_conn.get_listen_count_for_user(user_name) except (InfluxDBServerError, InfluxDBClientError): have_listen_count = False listen_count = 0 # Getting data for current page max_ts = request.args.get("max_ts") if max_ts is not None: try: max_ts = int(max_ts) except ValueError: raise BadRequest("Incorrect timestamp argument max_ts: %s" % request.args.get("max_ts")) min_ts = request.args.get("min_ts") if min_ts is not None: try: min_ts = int(min_ts) except ValueError: raise BadRequest("Incorrect timestamp argument min_ts: %s" % request.args.get("min_ts")) if max_ts is None and min_ts is None: max_ts = int(time.time()) args = {} if max_ts: args['to_ts'] = max_ts else: args['from_ts'] = min_ts listens = [] for listen in db_conn.fetch_listens(user_name, limit=LISTENS_PER_PAGE, **args): # Let's fetch one more listen, so we know to show a next page link or not listens.append({ "track_metadata": listen.data, "listened_at": listen.ts_since_epoch, "listened_at_iso": listen.timestamp.isoformat() + "Z", }) # Calculate if we need to show next/prev buttons previous_listen_ts = None next_listen_ts = None if listens: (min_ts_per_user, max_ts_per_user) = db_conn.get_timestamps_for_user(user_name) if min_ts_per_user >= 0: if listens[-1]['listened_at'] > min_ts_per_user: next_listen_ts = listens[-1]['listened_at'] else: next_listen_ts = None if listens[0]['listened_at'] < max_ts_per_user: previous_listen_ts = listens[0]['listened_at'] else: previous_listen_ts = None # If there are no previous listens then display now_playing if not previous_listen_ts: playing_now = playing_now_conn.get_playing_now(user.id) if playing_now: listen = { "track_metadata": playing_now.data, "playing_now": "true", } listens.insert(0, listen) user_stats = db_stats.get_user_artists(user.id) try: artist_count = int(user_stats['artist']['count']) except (KeyError, TypeError): artist_count = None return render_template( "user/profile.html", user=user, listens=listens, previous_listen_ts=previous_listen_ts, next_listen_ts=next_listen_ts, spotify_uri=_get_spotify_uri_for_listens(listens), have_listen_count=have_listen_count, listen_count=format(int(listen_count), ",d"), artist_count=format(artist_count, ",d") if artist_count else None, section='listens' )
def profile(user_name): # Which database to use to showing user listens. db_conn = webserver.influx_connection._influx # Which database to use to show playing_now stream. playing_now_conn = webserver.redis_connection._redis user = _get_user(user_name) # User name used to get user may not have the same case as original user name. user_name = user.musicbrainz_id try: have_listen_count = True listen_count = db_conn.get_listen_count_for_user(user_name) except (InfluxDBServerError, InfluxDBClientError): have_listen_count = False listen_count = 0 # Getting data for current page max_ts = request.args.get("max_ts") if max_ts is not None: try: max_ts = int(max_ts) except ValueError: raise BadRequest("Incorrect timestamp argument max_ts: %s" % request.args.get("max_ts")) min_ts = request.args.get("min_ts") if min_ts is not None: try: min_ts = int(min_ts) except ValueError: raise BadRequest("Incorrect timestamp argument min_ts: %s" % request.args.get("min_ts")) if max_ts is None and min_ts is None: max_ts = int(time.time()) args = {} if max_ts: args['to_ts'] = max_ts else: args['from_ts'] = min_ts listens = [] for listen in db_conn.fetch_listens(user_name, limit=LISTENS_PER_PAGE, **args): # Let's fetch one more listen, so we know to show a next page link or not listens.append({ "track_metadata": listen.data, "listened_at": listen.ts_since_epoch, "listened_at_iso": listen.timestamp.isoformat() + "Z", }) latest_listen = db_conn.fetch_listens(user_name=user_name, limit=1, to_ts=int(time.time())) latest_listen_ts = latest_listen[0].ts_since_epoch if len( latest_listen) > 0 else 0 # Calculate if we need to show next/prev buttons previous_listen_ts = None next_listen_ts = None if listens: (min_ts_per_user, max_ts_per_user) = db_conn.get_timestamps_for_user(user_name) if min_ts_per_user >= 0: if listens[-1]['listened_at'] > min_ts_per_user: next_listen_ts = listens[-1]['listened_at'] else: next_listen_ts = None if listens[0]['listened_at'] < max_ts_per_user: previous_listen_ts = listens[0]['listened_at'] else: previous_listen_ts = None # If there are no previous listens then display now_playing if not previous_listen_ts: playing_now = playing_now_conn.get_playing_now(user.id) if playing_now: listen = { "track_metadata": playing_now.data, "playing_now": "true", } listens.insert(0, listen) user_stats = db_stats.get_user_artists(user.id) try: artist_count = int(user_stats['artist']['count']) except (KeyError, TypeError): artist_count = None spotify_data = {} if current_user.is_authenticated: spotify_data = spotify.get_user_dict(current_user.id) props = { "user": { "id": user.id, "name": user.musicbrainz_id, }, "listens": listens, "previous_listen_ts": previous_listen_ts, "next_listen_ts": next_listen_ts, "latest_listen_ts": latest_listen_ts, "latest_spotify_uri": _get_spotify_uri_for_listens(listens), "have_listen_count": have_listen_count, "listen_count": format(int(listen_count), ",d"), "artist_count": format(artist_count, ",d") if artist_count else None, "profile_url": url_for('user.profile', user_name=user_name), "mode": "listens", "spotify": spotify_data, "web_sockets_server_url": current_app.config['WEBSOCKETS_SERVER_URL'], "api_url": current_app.config['API_URL'], } return render_template("user/profile.html", props=ujson.dumps(props), mode='listens', user=user, active_section='listens')