Beispiel #1
0
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,
        }
    })
Beispiel #2
0
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,
    }})
Beispiel #3
0
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,
        }
    })
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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,
        }
    })
Beispiel #7
0
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)
Beispiel #8
0
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,
        }
    })
Beispiel #9
0
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}})
Beispiel #10
0
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],
        }
    })