def get_recent_listens_for_user_list(user_list): """ Fetch the most recent listens for a comma separated list of users. Take care to properly HTTP escape user names that contain commas! :statuscode 200: Fetched listens successfully. :statuscode 400: Your user list was incomplete or otherwise invalid. :resheader Content-Type: *application/json* """ limit = _parse_int_arg("limit", 2) users = parse_param_list(user_list) if not len(users): raise APIBadRequest("user_list is empty or invalid.") db_conn = webserver.create_timescale(current_app) listens = db_conn.fetch_recent_listens_for_users(users, limit=limit) listen_data = [] for listen in listens: listen_data.append(listen.to_api()) return jsonify({ 'payload': { 'user_list': user_list, 'count': len(listen_data), 'listens': listen_data, } })
def user_feed(user_name: str): db_conn = webserver.create_timescale(current_app) min_ts, max_ts, count, time_range = _validate_get_endpoint_params(db_conn, user_name) if min_ts is None and max_ts is None: max_ts = int(time.time()) user = db_user.get_by_mb_id(user_name) if not user: raise APINotFound(f"User {user_name} not found") users_following = [user['musicbrainz_id'] for user in db_user_relationship.get_following_for_user(user['id'])] listens = db_conn.fetch_listens_for_multiple_users_from_storage( users_following, limit=count, from_ts=min_ts, to_ts=max_ts, time_range=time_range, order=1, # descending ) listen_data = [] for listen in listens: listen_data.append(listen.to_api()) return jsonify({'payload': { 'user_id': user_name, 'count': len(listen_data), 'feed': listen_data, }})
def get_listens(user_name): """ Get listens for user ``user_name``. The format for the JSON returned is defined in our :ref:`json-doc`. If none of the optional arguments are given, this endpoint will return the :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` most recent listens. The optional ``max_ts`` and ``min_ts`` UNIX epoch timestamps control at which point in time to start returning listens. You may specify max_ts or min_ts, but not both in one call. Listens are always returned in descending timestamp order. :param max_ts: If you specify a ``max_ts`` timestamp, listens with listened_at less than (but not including) this value will be returned. :param min_ts: If you specify a ``min_ts`` timestamp, listens with listened_at greater than (but not including) this value will be returned. :param count: Optional, number of listens to return. Default: :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` . Max: :data:`~webserver.views.api.MAX_ITEMS_PER_GET` :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* """ current_time = int(time.time()) max_ts = _parse_int_arg("max_ts") min_ts = _parse_int_arg("min_ts") time_range = _parse_int_arg("time_range", DEFAULT_TIME_RANGE) if time_range < 1 or time_range > MAX_TIME_RANGE: log_raise_400("time_range must be between 1 and %d." % MAX_TIME_RANGE) # if no max given, use now() if max_ts and min_ts: log_raise_400("You may only specify max_ts or min_ts, not both.") db_conn = webserver.create_timescale(current_app) (min_ts_per_user, max_ts_per_user) = db_conn.get_timestamps_for_user(user_name) # If none are given, start with now and go down if max_ts == None and min_ts == None: max_ts = max_ts_per_user + 1 # Validate requetsed listen count is positive count = min(_parse_int_arg("count", DEFAULT_ITEMS_PER_GET), MAX_ITEMS_PER_GET) if count < 0: log_raise_400("Number of listens requested should be positive") listens = db_conn.fetch_listens(user_name, limit=count, from_ts=min_ts, to_ts=max_ts, time_range=time_range) listen_data = [] for listen in listens: listen_data.append(listen.to_api()) return jsonify({ 'payload': { 'user_id': user_name, 'count': len(listen_data), 'listens': listen_data, 'latest_listen_ts': max_ts_per_user, } })
def get_listen_events( db_conn: TimescaleListenStore, musicbrainz_ids: List[str], min_ts: int, max_ts: int, count: int, time_range: int, ) -> List[APITimelineEvent]: """ Gets all listen events in the feed. """ # NOTE: For now, we get a bunch of listens for the users the current # user is following and take a max of 2 out of them per user. This # could be done better by writing a complex query to get exactly 2 listens for each user, # but I'm happy with this heuristic for now and we can change later. db_conn = webserver.create_timescale(current_app) listens = db_conn.fetch_listens_for_multiple_users_from_storage( musicbrainz_ids, limit=count, from_ts=min_ts, to_ts=max_ts, time_range=time_range, order=0, # descending ) user_listens_map = defaultdict(list) for listen in listens: if len(user_listens_map[ listen.user_name]) < MAX_LISTEN_EVENTS_PER_USER: user_listens_map[listen.user_name].append(listen) events = [] for user in user_listens_map: for listen in user_listens_map[user]: try: listen_dict = listen.to_api() listen_dict['inserted_at'] = listen_dict[ 'inserted_at'].timestamp() api_listen = APIListen(**listen_dict) events.append( APITimelineEvent( event_type=UserTimelineEventType.LISTEN, user_name=api_listen.user_name, created=api_listen.listened_at, metadata=api_listen, )) except pydantic.ValidationError as e: current_app.logger.error('Validation error: ' + str(e), exc_info=True) continue return events
def fetch_listens(musicbrainz_id, to_ts, time_range=None): """ Fetch all listens for the user from listenstore by making repeated queries to listenstore until we get all the data. Returns a generator that streams the results. """ db_conn = webserver.create_timescale(current_app) while True: batch = db_conn.fetch_listens(current_user.musicbrainz_id, to_ts=to_ts, limit=EXPORT_FETCH_COUNT, time_range=time_range) if not batch: break yield from batch to_ts = batch[-1].ts_since_epoch # new to_ts will be the the timestamp of the last listen fetched
def get_listens(user_name): """ Get listens for user ``user_name``. The format for the JSON returned is defined in our :ref:`json-doc`. If none of the optional arguments are given, this endpoint will return the :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` most recent listens. The optional ``max_ts`` and ``min_ts`` UNIX epoch timestamps control at which point in time to start returning listens. You may specify max_ts or min_ts, but not both in one call. Listens are always returned in descending timestamp order. :param max_ts: If you specify a ``max_ts`` timestamp, listens with listened_at less than (but not including) this value will be returned. :param min_ts: If you specify a ``min_ts`` timestamp, listens with listened_at greater than (but not including) this value will be returned. :param count: Optional, number of listens to return. Default: :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` . Max: :data:`~webserver.views.api.MAX_ITEMS_PER_GET` :param time_range: This parameter determines the time range for the listen search. Each increment of the time_range corresponds to a range of 5 days and the default time_range of 3 means that 15 days will be searched. Default: :data:`~webserver.views.api.DEFAULT_TIME_RANGE` . Max: :data:`~webserver.views.api.MAX_TIME_RANGE` :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* """ db_conn = webserver.create_timescale(current_app) min_ts, max_ts, count, time_range = _validate_get_endpoint_params( db_conn, user_name) _, max_ts_per_user = db_conn.get_timestamps_for_user(user_name) # If none are given, start with now and go down if max_ts == None and min_ts == None: max_ts = max_ts_per_user + 1 listens = db_conn.fetch_listens(user_name, limit=count, from_ts=min_ts, to_ts=max_ts, time_range=time_range) listen_data = [] for listen in listens: listen_data.append(listen.to_api()) return jsonify({ 'payload': { 'user_id': user_name, 'count': len(listen_data), 'listens': listen_data, 'latest_listen_ts': max_ts_per_user, } })
def export_data(): """ Exporting the data to json """ if request.method == "POST": db_conn = webserver.create_timescale(current_app) filename = current_user.musicbrainz_id + "_lb-" + datetime.today().strftime('%Y-%m-%d') + ".json" # Build a generator that streams the json response. We never load all # listens into memory at once, and we can start serving the response # immediately. to_ts = int(time()) listens = fetch_listens(current_user.musicbrainz_id, to_ts, time_range=-1) output = stream_json_array(listen.to_api() for listen in listens) response = Response(stream_with_context(output)) response.headers["Content-Disposition"] = "attachment; filename=" + filename response.headers['Content-Type'] = 'application/json; charset=utf-8' response.mimetype = "text/json" return response else: return render_template("user/export.html", user=current_user)
def get_listens(user_name): """ Get listens for user ``user_name``. The format for the JSON returned is defined in our :ref:`json-doc`. If none of the optional arguments are given, this endpoint will return the :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` most recent listens. The optional ``max_ts`` and ``min_ts`` UNIX epoch timestamps control at which point in time to start returning listens. You may specify max_ts or min_ts, but not both in one call. Listens are always returned in descending timestamp order. :param max_ts: If you specify a ``max_ts`` timestamp, listens with listened_at less than (but not including) this value will be returned. :param min_ts: If you specify a ``min_ts`` timestamp, listens with listened_at greater than (but not including) this value will be returned. :param count: Optional, number of listens to return. Default: :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` . Max: :data:`~webserver.views.api.MAX_ITEMS_PER_GET` :statuscode 200: Yay, you have data! :statuscode 404: The requested user was not found. :resheader Content-Type: *application/json* """ db_conn = webserver.create_timescale(current_app) user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: %s" % user_name) min_ts, max_ts, count = _validate_get_endpoint_params() if min_ts and max_ts and min_ts >= max_ts: raise APIBadRequest("min_ts should be less than max_ts") listens, _, max_ts_per_user = db_conn.fetch_listens(user_name, limit=count, from_ts=min_ts, to_ts=max_ts) listen_data = [] for listen in listens: listen_data.append(listen.to_api()) return jsonify({ 'payload': { 'user_id': user_name, 'count': len(listen_data), 'listens': listen_data, 'latest_listen_ts': max_ts_per_user, } })
def get_listen_count(user_name): """ Get the number of listens for a user ``user_name``. The returned listen count has an element 'payload' with only key: 'count' which unsurprisingly contains the listen count for the user. :statuscode 200: Yay, you have listen counts! :resheader Content-Type: *application/json* """ try: db_conn = webserver.create_timescale(current_app) listen_count = db_conn.get_listen_count_for_user(user_name) if listen_count < 0: raise APINotFound("Cannot find user: %s" % user_name) except psycopg2.OperationalError as err: current_app.logger.error("cannot fetch user listen count: ", str(err)) raise APIServiceUnavailable( "Cannot fetch user listen count right now.") return jsonify({'payload': {'count': listen_count}})
def user_feed(user_name: str): """ Get feed events for a user's timeline. :param user_name: The MusicBrainz ID of the user whose timeline is being requested. :type user_name: ``str`` :param max_ts: If you specify a ``max_ts`` timestamp, events with timestamps less than the value will be returned :param min_ts: If you specify a ``min_ts`` timestamp, events with timestamps greater than the value will be returned :param count: Optional, number of events to return. Default: :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` . Max: :data:`~webserver.views.api.MAX_ITEMS_PER_GET` :statuscode 200: Successful query, you have feed events! :statuscode 400: Bad request, check ``response['error']`` for more details. :statuscode 401: Unauthorized, you do not have permission to view this user's feed. :statuscode 404: User not found :resheader Content-Type: *application/json* """ user = validate_auth_header() if user_name != user['musicbrainz_id']: raise APIUnauthorized( "You don't have permissions to view this user's timeline.") db_conn = webserver.create_timescale(current_app) min_ts, max_ts, count, time_range = _validate_get_endpoint_params( db_conn, user_name) if min_ts is None and max_ts is None: max_ts = int(time.time()) users_following = db_user_relationship.get_following_for_user(user['id']) # get all listen events musicbrainz_ids = [user['musicbrainz_id'] for user in users_following] if len(users_following) == 0: listen_events = [] else: listen_events = get_listen_events(db_conn, musicbrainz_ids, min_ts, max_ts, count, time_range) # for events like "follow" and "recording recommendations", we want to show the user # their own events as well users_for_feed_events = users_following + [user] follow_events = get_follow_events( user_ids=tuple(user['id'] for user in users_for_feed_events), min_ts=min_ts or 0, max_ts=max_ts or int(time.time()), count=count, ) recording_recommendation_events = get_recording_recommendation_events( users_for_events=users_for_feed_events, min_ts=min_ts or 0, max_ts=max_ts or int(time.time()), count=count, ) notification_events = get_notification_events(user, count) # TODO: add playlist event and like event all_events = sorted(listen_events + follow_events + recording_recommendation_events + notification_events, key=lambda event: -event.created) # sadly, we need to serialize the event_type ourselves, otherwise, jsonify converts it badly for index, event in enumerate(all_events): all_events[index].event_type = event.event_type.value all_events = all_events[:count] return jsonify({ 'payload': { 'count': len(all_events), 'user_id': user_name, 'events': [event.dict() for event in all_events], } })