def test_insert_user_stats_mult_ranges_recording(self): """ Test if multiple time range data is inserted correctly """ with open(self.path_to_data_file('user_top_recordings_db.json')) as f: recordings_data = json.load(f) recordings_data_year = deepcopy(recordings_data) recordings_data_year['stats_range'] = 'year' db_stats.insert_user_jsonb_data( user_id=self.user['id'], stats_type='recordings', stats=StatRange[UserEntityRecord](**recordings_data)) db_stats.insert_user_jsonb_data( user_id=self.user['id'], stats_type='recordings', stats=StatRange[UserEntityRecord](**recordings_data_year)) result = db_stats.get_user_stats(user_id=self.user['id'], stats_range='all_time', stats_type='recordings') self.assertDictEqual(result.dict(exclude={'user_id', 'last_updated'}), recordings_data) result = db_stats.get_user_stats(user_id=self.user['id'], stats_range='year', stats_type='recordings') self.assertDictEqual(result.dict(exclude={'user_id', 'last_updated'}), recordings_data_year)
def test_get_user_stats(self): data_inserted = self.insert_test_data() data = db_stats.get_user_stats(self.user['id'], 'artist') self.assertEqual(data['artist']['count'], 2) data = db_stats.get_user_stats(self.user['id'], 'recording') self.assertListEqual(data['recording']['all_time']['recordings'], data_inserted['user_recordings'])
def test_get_user_stats(self): data_inserted = self.insert_test_data() data = db_stats.get_user_stats(self.user['id'], 'artist') self.assertEqual(data['artist']['count'], 2) data = db_stats.get_user_stats(self.user['id'], 'recording') self.assertListEqual(data['recording']['all_time'], data_inserted['user_recordings'])
def _get_entity_stats(user_name: str, entity: str, count_key: str): user, stats_range = _validate_stats_user_params(user_name) offset = get_non_negative_param("offset", default=0) count = get_non_negative_param("count", default=DEFAULT_ITEMS_PER_GET) stats = db_stats.get_user_stats(user["id"], stats_range, entity) if stats is None: raise APINoContent('') entity_list, total_entity_count = _process_user_entity( stats, offset, count) return jsonify({ "payload": { "user_id": user_name, entity: entity_list, "count": len(entity_list), count_key: total_entity_count, "offset": offset, "range": stats_range, "from_ts": stats.from_ts, "to_ts": stats.to_ts, "last_updated": int(stats.last_updated.timestamp()), } })
def test_insert_user_recordings(self): """ Test if recording stats are inserted correctly """ with open(self.path_to_data_file('user_top_recordings_db.json')) as f: recordings_data = json.load(f) db_stats.insert_user_jsonb_data(user_id=self.user['id'], stats_type='recordings', stats=StatRange[EntityRecord](**recordings_data)) result = db_stats.get_user_stats(user_id=self.user['id'], stats_range='all_time', stats_type='recordings') self.assertDictEqual(result.dict(exclude={'user_id', 'last_updated'}), recordings_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_jsonb_data( user_id=user_id, stats_type='artists', stats=StatRange[EntityRecord](**artists_data), ) user_stats = db_stats.get_user_stats(user_id, 'all_time', 'artists') self.assertIsNotNone(user_stats) db_user.delete(user_id) user = db_user.get(user_id) self.assertIsNone(user) user_stats = db_stats.get_user_stats(user_id, 'all_time', 'artists') self.assertIsNone(user_stats)
def _get_artist_map_stats(user_id, 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: calculated = False # Check if the stats present in DB have been calculated in the past week, if not recalculate them stale = False if calculated: if (datetime.now(timezone.utc) - stats.last_updated).days >= STATS_CALCULATION_INTERVAL: stale = True if stale or not calculated: artist_stats = db_stats.get_user_stats(user_id, stats_range, 'artists') # If top artists are missing, return the stale stats if present, otherwise return 204 if artist_stats is None: if stale: result = stats else: raise APINoContent('') else: # Calculate the data artist_mbid_counts = defaultdict(int) for artist in artist_stats.data.__root__: for artist_mbid in artist.artist_mbids: artist_mbid_counts[artist_mbid] += artist.listen_count country_code_data = _get_country_wise_counts(artist_mbid_counts) result = StatApi[UserArtistMapRecord]( **{ "data": country_code_data, "from_ts": artist_stats.from_ts, "to_ts": artist_stats.to_ts, "stats_range": stats_range, # this isn't stored in the database, but adding it here to avoid a subsequent db call to # just fetch last updated. could just store this value instead in db but that complicates # the implementation a bit "last_updated": datetime.now(), "user_id": user_id }) # Store in DB for future use try: db_stats.insert_user_jsonb_data(user_id, 'artist_map', result) except Exception as err: current_app.logger.error( "Error while inserting artist map stats for {user}. Error: {err}. Data: {data}" .format(user=user_id, err=err, data=result), exc_info=True) else: result = stats return result
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: .. code-block:: json { "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 force_recalculate: ``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, stats_range = _validate_stats_user_params(user_name) 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: calculated = False # Check if the stats present in DB have been calculated in the past week, if not recalculate them stale = False if calculated: if (datetime.now(timezone.utc) - stats.last_updated).days >= STATS_CALCULATION_INTERVAL: stale = True if stale or not calculated: artist_stats = db_stats.get_user_stats(user['id'], stats_range, 'artists') # If top artists are missing, return the stale stats if present, otherwise return 204 if artist_stats is None: if stale: result = stats else: raise APINoContent('') else: # Calculate the data artist_mbid_counts = defaultdict(int) for artist in artist_stats.data.__root__: for artist_mbid in artist.artist_mbids: artist_mbid_counts[artist_mbid] += artist.listen_count country_code_data = _get_country_wise_counts(artist_mbid_counts) result = StatApi[UserArtistMapRecord]( **{ "data": country_code_data, "from_ts": artist_stats.from_ts, "to_ts": artist_stats.to_ts, "stats_range": stats_range, # this isn't stored in the database, but adding it here to avoid a subsequent db call to # just fetch last updated. could just store this value instead in db but that complicates # the implementation a bit "last_updated": datetime.now(), "user_id": user['id'] }) # Store in DB for future use try: db_stats.insert_user_jsonb_data(user['id'], 'artist_map', 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, "from_ts": result.from_ts, "to_ts": result.to_ts, "last_updated": int(result.last_updated.timestamp()), "artist_map": [x.dict() for x in result.data.__root__] } })
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")) args = {} if max_ts: args['to_ts'] = max_ts else: args['from_ts'] = min_ts data, min_ts_per_user, max_ts_per_user = db_conn.fetch_listens( user_name, limit=LISTENS_PER_PAGE, **args) listens = [] for listen in data: listens.append(listen.to_api()) # 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_stats(user.id, 'all_time', 'artists') logged_in_user_follows_user = None already_reported_user = False if current_user.is_authenticated: logged_in_user_follows_user = db_user_relationship.is_following_user( current_user.id, user.id) already_reported_user = db_user.is_user_reported( current_user.id, user.id) pin = get_current_pin_for_user(user_id=user.id) if pin: pin = dict(fetch_track_metadata_for_pins([pin])[0]) props = { "user": { "id": user.id, "name": user.musicbrainz_id, }, "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(user_stats.count, ",d") if user_stats else None, "profile_url": url_for('user.profile', user_name=user_name), "mode": "listens", "userPinnedRecording": pin, "web_sockets_server_url": current_app.config['WEBSOCKETS_SERVER_URL'], "logged_in_user_follows_user": logged_in_user_follows_user, "already_reported_user": already_reported_user, } return render_template("user/profile.html", props=ujson.dumps(props), mode='listens', user=user, active_section='listens')
def test_get_user_recordings(self): data_inserted = self.insert_test_data() result = db_stats.get_user_stats(self.user['id'], 'all_time', 'recordings') self.assertDictEqual(result.dict(exclude={'user_id', 'last_updated'}), data_inserted['user_recordings'])
def test_handle_user_entity(self): data = { 'type': 'user_entity', 'entity': 'artists', 'stats_range': 'all_time', 'from_ts': 1, 'to_ts': 10, 'data': [{ 'user_id': 1, 'data': [{ 'artist_name': 'Kanye West', 'listen_count': 200, }], 'count': 1, }, { 'user_id': 2, 'data': [{ 'artist_name': 'Selena Gomez', 'listen_count': 100, }, { 'artist_name': 'Tom Ellis', 'listen_count': 50, }], 'count': 2, }] } handle_user_entity(data) received = db_stats.get_user_stats(1, 'all_time', 'artists') expected = StatApi[EntityRecord]( user_id=1, to_ts=10, from_ts=1, count=1, stats_range='all_time', data=StatRecordList[EntityRecord](__root__=[ ArtistRecord( artist_mbids=[], listen_count=200, artist_name='Kanye West', ) ]), last_updated=received.last_updated) self.assertEqual(received, expected) received = db_stats.get_user_stats(2, 'all_time', 'artists') expected = StatApi[EntityRecord]( user_id=2, to_ts=10, from_ts=1, count=2, stats_range='all_time', data=StatRecordList[EntityRecord](__root__=[ ArtistRecord( artist_mbids=[], listen_count=100, artist_name='Selena Gomez', ), ArtistRecord( artist_mbids=[], listen_count=50, artist_name='Tom Ellis', ) ]), last_updated=received.last_updated) self.assertEqual(received, expected)