Example #1
0
def get_playlist(playlist_mbid):
    """
    Fetch the given playlist.

    :param playlist_mbid: The playlist mbid to fetch.
    :type playlist_mbid: ``str``
    :param fetch_metadata: Optional, pass value 'false' to skip lookup up recording metadata
    :type fetch_metadata: ``bool``
    :statuscode 200: Yay, you have data!
    :statuscode 404: Playlist not found
    :statuscode 401: Invalid authorization. See error message for details.
    :resheader Content-Type: *application/json*
    """

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    fetch_metadata = parse_boolean_arg("fetch_metadata", True)

    playlist = db_playlist.get_by_mbid(playlist_mbid, True)
    if playlist is None:
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    user = validate_auth_header(optional=True)
    user_id = None
    if user:
        user_id = user["id"]
    if not playlist.is_visible_by(user_id):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if fetch_metadata:
        fetch_playlist_recording_metadata(playlist)

    return jsonify(serialize_jspf(playlist))
Example #2
0
def get_playlist(playlist_mbid):
    """
    Fetch the given playlist.

    :param playlist_mbid: Optional, The playlist mbid to fetch.
    :statuscode 200: Yay, you have data!
    :statuscode 404: Playlist not found
    :statuscode 401: Invalid authorization. See error message for details.
    :resheader Content-Type: *application/json*
    """

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid, True)
    if playlist is None:
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if not playlist.public:
        user = validate_auth_header()
        if playlist.creator_id != user["id"]:
            raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    fetch_playlist_recording_metadata(playlist)

    return jsonify(serialize_jspf(playlist))
Example #3
0
def delete_playlist(playlist_mbid):
    """

    Delete a playlist. POST body data does not need to contain anything.

    :reqheader Authorization: Token <user token>
    :statuscode 200: playlist deleted.
    :statuscode 401: invalid authorization. See error message for details.
    :statuscode 403: forbidden. the requesting user was not allowed to carry out this operation.
    :statuscode 404: Playlist not found
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid)
    if playlist is None or not playlist.is_visible_by(user["id"]):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if playlist.creator_id != user["id"]:
        raise APIForbidden("You are not allowed to delete this playlist.")

    try:
        db_playlist.delete_playlist(playlist)
    except Exception as e:
        current_app.logger.error("Error deleting playlist: {}".format(e))
        raise APIInternalServerError("Failed to delete the playlist. Please try again.")

    return jsonify({'status': 'ok'})
def load_playlist(playlist_mbid: str):
    """Load a single playlist by id
    """
    if not is_valid_uuid(playlist_mbid):
        raise BadRequest("Provided playlist ID is invalid: %s" % playlist_mbid)

    playlist = db_playlist.get_by_mbid(playlist_mbid, True)
    # TODO: Allow playlist collaborators to access private playlist
    if playlist is None or not playlist.public and (not current_user.is_authenticated or playlist.creator_id != current_user.id):
        raise NotFound("Cannot find playlist: %s" % playlist_mbid)

    fetch_playlist_recording_metadata(playlist)

    spotify_data = {}
    current_user_data = {}
    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,
        }
    props = {
        "current_user": current_user_data,
        "spotify": spotify_data,
        "api_url": current_app.config["API_URL"],
        "web_sockets_server_url": current_app.config['WEBSOCKETS_SERVER_URL'],
        "playlist": serialize_jspf(playlist)
    }

    return render_template(
        "playlists/playlist.html",
        props=ujson.dumps(props)
    )
Example #5
0
def copy_playlist(playlist_mbid):
    """

    Copy a playlist -- the new playlist will be given the name "Copy of <playlist_name>".
    POST body data does not need to contain anything.

    :reqheader Authorization: Token <user token>
    :statuscode 200: playlist copied.
    :statuscode 401: invalid authorization. See error message for details.
    :statuscode 404: Playlist not found
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid)
    if playlist is None or not playlist.is_visible_by(user["id"]):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    try:
        new_playlist = db_playlist.copy_playlist(playlist, user["id"])
    except Exception as e:
        current_app.logger.error("Error copying playlist: {}".format(e))
        raise APIInternalServerError("Failed to copy the playlist. Please try again.")

    return jsonify({'status': 'ok', 'playlist_mbid': new_playlist.mbid})
Example #6
0
def delete_listen():
    """
    Delete a particular listen from a user's listen history.
    This checks for the correct authorization token and deletes the listen.

    .. note::

        The listen is not deleted immediately, but is scheduled for deletion, which
        usually happens shortly after the hour.

    The format of the JSON to be POSTed to this endpoint is:

    .. code-block:: json

        {
            "listened_at": 1,
            "recording_msid": "d23f4719-9212-49f0-ad08-ddbfbfc50d6f"
        }

    :reqheader Authorization: Token <user token>
    :reqheader Content-Type: *application/json*
    :statuscode 200: listen deleted.
    :statuscode 400: invalid JSON sent, see error message for details.
    :statuscode 401: invalid authorization. See error message for details.
    :resheader Content-Type: *application/json*
    """
    user = validate_auth_header()

    data = request.json

    if "listened_at" not in data:
        log_raise_400("Listen timestamp missing.")
    try:
        listened_at = data["listened_at"]
        listened_at = int(listened_at)
    except ValueError:
        log_raise_400("%s: Listen timestamp invalid." % listened_at)

    if "recording_msid" not in data:
        log_raise_400("Recording MSID missing.")

    recording_msid = data["recording_msid"]
    if not is_valid_uuid(recording_msid):
        log_raise_400("%s: Recording MSID format invalid." % recording_msid)

    try:
        timescale_connection._ts.delete_listen(listened_at=listened_at,
                                               recording_msid=recording_msid, user_id=user["id"])
    except TimescaleListenStoreException as e:
        current_app.logger.error("Cannot delete listen for user: %s" % str(e))
        raise APIServiceUnavailable(
            "We couldn't delete the listen. Please try again later.")
    except Exception as e:
        current_app.logger.error("Cannot delete listen for user: %s" % str(e))
        raise APIInternalServerError(
            "We couldn't delete the listen. Please try again later.")

    return jsonify({'status': 'ok'})
Example #7
0
def add_playlist_item(playlist_mbid, offset):
    """
    Append recordings to an existing playlist by posting a playlist with one of more recordings in it.
    The playlist must be in JSPF format with MusicBrainz extensions, which is defined here:
    https://musicbrainz.org/doc/jspf .

    If the offset is provided in the URL, then the recordings will be added at that offset,
    otherwise they will be added at the end of the playlist.

    You may only add :data:`~webserver.views.playlist_api.MAX_RECORDINGS_PER_ADD` recordings in one
    call to this endpoint.

    :reqheader Authorization: Token <user token>
    :statuscode 200: playlist accepted.
    :statuscode 400: invalid JSON sent, see error message for details.
    :statuscode 401: invalid authorization. See error message for details.
    :statuscode 403: forbidden. the requesting user was not allowed to carry out this operation.
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()
    if offset is not None and offset < 0:
        log_raise_400("Offset must be a positive integer.")

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid)
    if playlist is None or not playlist.is_visible_by(user["id"]):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if not playlist.is_modifiable_by(user["id"]):
        raise APIForbidden("You are not allowed to add recordings to this playlist.")

    data = request.json
    validate_playlist(data)

    if len(data["playlist"]["track"]) > MAX_RECORDINGS_PER_ADD:
        log_raise_400("You may only add max %d recordings per call." % MAX_RECORDINGS_PER_ADD)

    precordings = []
    if "track" in data["playlist"]:
        for track in data["playlist"]["track"]:
            try:
                mbid = UUID(track['identifier'][len(PLAYLIST_TRACK_URI_PREFIX):])
            except (KeyError, ValueError):
                log_raise_400("Track %d has an invalid identifier field, it must be a complete URI.")
            precordings.append(WritablePlaylistRecording(mbid=mbid, added_by_id=user["id"]))

    try:
        db_playlist.add_recordings_to_playlist(playlist, precordings, offset)
    except Exception as e:
        current_app.logger.error("Error while adding recordings to playlist: {}".format(e))
        raise APIInternalServerError("Failed to add recordings to the playlist. Please try again.")

    return jsonify({'status': 'ok'})
Example #8
0
def validate_playlist(jspf, require_title=True):
    """
        Given a JSPF dict, ensure that title is present and that if tracks are present
        they have valid URIs or MBIDs specified. If any errors are found 400 is raised.
    """

    if require_title:
        try:
            title = jspf["playlist"]["title"]
            if not title:
                raise KeyError
        except KeyError:
            log_raise_400(
                "JSPF playlist must contain a title element with the title of the playlist."
            )

    try:
        public = jspf["playlist"]["extension"][PLAYLIST_EXTENSION_URI][
            "public"]
        if not isinstance(public, bool):
            log_raise_400("JSPF playlist public field must contain a boolean.")
    except KeyError:
        log_raise_400(
            "JSPF playlist.extension.https://musicbrainz.org/doc/jspf#playlist.public field must be given."
        )

    try:
        for collaborator in jspf["playlist"]["extension"][
                PLAYLIST_EXTENSION_URI]["collaborators"]:
            if not collaborator:
                log_raise_400(
                    "The collaborators field contains an empty value.")
    except KeyError:
        pass

    if 'track' not in jspf:
        return

    for i, track in enumerate(jspf.get('track', [])):
        recording_uri = track.get("identifier")
        if not recording_uri:
            log_raise_400(
                "JSPF playlist track %d must contain an identifier element with recording MBID."
                % i)

        if recording_uri.startswith(PLAYLIST_TRACK_URI_PREFIX):
            recording_mbid = recording_uri[len(PLAYLIST_TRACK_URI_PREFIX):]
        else:
            log_raise_400(
                "JSPF playlist track %d identifier must have the namespace '%s' prepended to it."
                % (i, PLAYLIST_TRACK_URI_PREFIX))

        if not is_valid_uuid(recording_mbid):
            log_raise_400(
                "JSPF playlist track %d does not contain a valid track identifier field."
                % i)
    def test_get_listens(self):
        """ Test to make sure that the api sends valid listens on get requests.
        """
        with open(self.path_to_data_file('valid_single.json'), 'r') as f:
            payload = json.load(f)

        # send a listen
        payload['payload'][0]['listened_at'] = int(time.time())
        response = self.send_data(payload)
        self.assert200(response)
        self.assertEqual(response.json['status'], 'ok')

        # This sleep allows for the influx subscriber to take its time in getting
        # the listen submitted from redis and writing it to influx.
        # Removing it causes an empty list of listens to be returned.
        time.sleep(2)

        url = url_for('api_v1.get_listens',
                      user_name=self.user['musicbrainz_id'])
        response = self.client.get(url, query_string={'count': '1'})
        self.assert200(response)
        data = json.loads(response.data)['payload']

        # make sure user id is correct
        self.assertEqual(data['user_id'], self.user['musicbrainz_id'])

        # make sure that count is 1 and list also contains 1 listen
        self.assertEqual(data['count'], 1)
        self.assertEqual(len(data['listens']), 1)

        # make sure timestamp is the same as sent
        sent_time = payload['payload'][0]['listened_at']
        self.assertEqual(data['listens'][0]['listened_at'], sent_time)

        # make sure that artist msid, release msid and recording msid are present in data
        self.assertTrue(is_valid_uuid(data['listens'][0]['recording_msid']))
        self.assertTrue(
            is_valid_uuid(data['listens'][0]['track_metadata']
                          ['additional_info']['artist_msid']))
        self.assertTrue(
            is_valid_uuid(data['listens'][0]['track_metadata']
                          ['additional_info']['release_msid']))
    def test_get_listens(self):
        """ Test to make sure that the api sends valid listens on get requests.
        """
        with open(self.path_to_data_file('valid_single.json'), 'r') as f:
            payload = json.load(f)

        # send a listen
        payload['payload'][0]['listened_at'] = int(time.time())
        response = self.send_data(payload)
        self.assert200(response)
        self.assertEqual(response.json['status'], 'ok')

        # This sleep allows for the influx subscriber to take its time in getting
        # the listen submitted from redis and writing it to influx.
        # Removing it causes an empty list of listens to be returned.
        time.sleep(2)

        url = url_for('api_v1.get_listens', user_name = self.user['musicbrainz_id'])
        response = self.client.get(url, query_string = {'count': '1'})
        self.assert200(response)
        data = json.loads(response.data)['payload']

        # make sure user id is correct
        self.assertEqual(data['user_id'], self.user['musicbrainz_id'])

        # make sure that count is 1 and list also contains 1 listen
        self.assertEqual(data['count'], 1)
        self.assertEqual(len(data['listens']), 1)

        # make sure timestamp is the same as sent
        sent_time = payload['payload'][0]['listened_at']
        self.assertEqual(data['listens'][0]['listened_at'], sent_time)
        self.assertEqual(data['listens'][0]['track_metadata']['track_name'], 'Fade')
        self.assertEqual(data['listens'][0]['track_metadata']['artist_name'], 'Kanye West')
        self.assertEqual(data['listens'][0]['track_metadata']['release_name'], 'The Life of Pablo')

        # make sure that artist msid, release msid and recording msid are present in data
        self.assertTrue(is_valid_uuid(data['listens'][0]['recording_msid']))
        self.assertTrue(is_valid_uuid(data['listens'][0]['track_metadata']['additional_info']['artist_msid']))
        self.assertTrue(is_valid_uuid(data['listens'][0]['track_metadata']['additional_info']['release_msid']))
Example #11
0
def load_instant():
    """
    This endpoint takes in a list of recording_mbids and optional desc/name arguments  and then loads
    the recording_mbid's metadata and creates a JSPF file from this data and sends it to the front end
    so a playlist can be instantly played.

    .. note::
        We recommend that you do not send more than 50 recording_mbids in one request -- our
        server infrastructure will likely give you a gateway error (502) if you do.

    :param recording_mbids: A comma separated list of recording_mbids
    :type recording_mbids: ``str``
    :param desc: A description for this instant playlist (optional).
    :type desc: ``str``
    :param name: A name for this instant playlist (optional).
    :type name: ``str``
    :statuscode 200: playlist generated
    :statuscode 400: invalid recording_mbid arguments
    """

    recordings = request.args.get("recording_mbids", default=None)
    if recordings is None:
        raise BadRequest("recording_mbids argument must be present and contain a comma separated list of recording_mbids")

    recording_mbids = []
    for mbid in recordings.split(","):
        mbid_clean = mbid.strip()
        if not is_valid_uuid(mbid_clean):
            raise BadRequest(f"Recording mbid {mbid} is not valid.")

        recording_mbids.append(mbid_clean)

    desc = request.args.get("desc", default="")
    if not desc:
        desc = "Instant playlist"

    name = request.args.get("name", default="")
    if not name:
        name = "Instant playlist"

    now = datetime.now()
    playlist = WritablePlaylist(description=desc, name=name, creator="listenbrainz", creator_id=1, created=now)
    for i, mbid in enumerate(recording_mbids):
        rec = WritablePlaylistRecording(position=i, mbid=mbid, added_by_id=1, created=now)
        playlist.recordings.append(rec)

    fetch_playlist_recording_metadata(playlist)

    return render_template(
        "player/player-page.html",
        props=ujson.dumps({"playlist": serialize_jspf(playlist)})
    )
def move_playlist_item(playlist_mbid):
    """

    To move an item in a playlist, the POST data needs to specify the recording MBID and current index
    of the track to move (from), where to move it to (to) and how many tracks from that position should
    be moved (count). The format of the post data should look as follows:

    .. code-block:: json

        {
            "mbid": "<mbid>",
            "from": 3,
            "to": 4,
            "count": 2
        }

    :reqheader Authorization: Token <user token>
    :statuscode 200: move operation succeeded
    :statuscode 400: invalid JSON sent, see error message for details.
    :statuscode 401: invalid authorization. See error message for details.
    :statuscode 403: forbidden. the requesting user was not allowed to carry out this operation.
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid)
    if playlist is None or not playlist.is_visible_by(user["id"]):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if not playlist.is_modifiable_by(user["id"]):
        raise APIForbidden(
            "You are not allowed to move recordings in this playlist.")

    data = request.json
    validate_move_data(data)

    try:
        db_playlist.move_recordings(playlist, data['from'], data['to'],
                                    data['count'])
    except Exception as e:
        current_app.logger.error(
            "Error while moving recordings in the playlist: {}".format(e))
        raise APIInternalServerError(
            "Failed to move recordings in the playlist. Please try again.")

    return jsonify({'status': 'ok'})
def get_feedback_for_recording(recording_msid):
    """
    Get feedback for recording with given ``recording_msid``. The format for the JSON returned
    is defined in our :ref:`feedback-json-doc`.

    :param score: Optional, If 1 then returns the loved recordings, if -1 returns hated recordings.
    :type score: ``int``
    :param count: Optional, number of feedback items 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 feedback items to skip from the beginning, for pagination.
        Ex. An offset of 5 means the top 5 feedback will be skipped, defaults to 0.
    :type offset: ``int``
    :statuscode 200: Yay, you have data!
    :resheader Content-Type: *application/json*
    """

    if not is_valid_uuid(recording_msid):
        log_raise_400("%s MSID format invalid." % recording_msid)

    score = _parse_int_arg('score')

    offset = _get_non_negative_param('offset', default=0)
    count = _get_non_negative_param('count', default=DEFAULT_ITEMS_PER_GET)

    count = min(count, MAX_ITEMS_PER_GET)

    if score:
        if score not in [-1, 1]:
            log_raise_400("Score can have a value of 1 or -1.", request.args)

    feedback = db_feedback.get_feedback_for_recording(
        recording_msid=recording_msid, limit=count, offset=offset, score=score)
    total_count = db_feedback.get_feedback_count_for_recording(recording_msid)

    for i, fb in enumerate(feedback):
        fb.user_id = fb.user_name
        del fb.user_name
        feedback[i] = fb.dict()

    return jsonify({
        "feedback": feedback,
        "count": len(feedback),
        "total_count": total_count,
        "offset": offset
    })
Example #14
0
def delete_playlist_item(playlist_mbid):
    """

    To delete an item in a playlist, the POST data needs to specify the recording MBID and current index
    of the track to delete, and how many tracks from that position should be moved deleted. The format of the
    post data should look as follows:

    .. code-block:: json
      {“index” : 3, “count”: 2}

    :reqheader Authorization: Token <user token>
    :statuscode 200: playlist accepted.
    :statuscode 400: invalid JSON sent, see error message for details.
    :statuscode 401: invalid authorization. See error message for details.
    :statuscode 403: forbidden. the requesting user was not allowed to carry out this operation.
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid)
    if playlist is None or \
       (playlist.creator_id != user["id"] and not playlist.public):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if playlist.creator_id != user["id"]:
        raise APIForbidden(
            "You are not allowed to remove recordings from this playlist.")

    data = request.json
    validate_delete_data(data)

    try:
        db_playlist.delete_recordings_from_playlist(playlist, data['index'],
                                                    data['count'])
    except Exception as e:
        current_app.logger.error(
            "Error while deleting recordings from playlist: {}".format(e))
        raise APIInternalServerError(
            "Failed to deleting recordings from the playlist. Please try again."
        )

    return jsonify({'status': 'ok'})
def get_feedback_for_recording_msid(recording_msid):
    """
    Get feedback for recording with given ``recording_msid``. The format for the JSON returned is defined in
    our :ref:`feedback-json-doc`.

    :param score: Optional, If 1 then returns the loved recordings, if -1 returns hated recordings.
    :type score: ``int``
    :param count: Optional, number of feedback items 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 feedback items to skip from the beginning, for pagination.
        Ex. An offset of 5 means the top 5 feedback will be skipped, defaults to 0.
    :type offset: ``int``
    :statuscode 200: Yay, you have data!
    :resheader Content-Type: *application/json*
    """
    if not is_valid_uuid(recording_msid):
        log_raise_400(f"{recording_msid} msid format invalid.")
    return _get_feedback_for_recording("recording_msid", recording_msid)
Example #16
0
def validate_move_data(data):
    """
        Check that the passed JSON for a move recordings endpoint call are valid. Raise 400 with
        error message if not.
    """

    if "mbid" not in data or "from" not in data or "to" not in data or "count" not in data:
        log_raise_400("post data for a move instruction must include the keys 'from', 'to', 'count' and 'mbid'.")

    if not is_valid_uuid(data["mbid"]):
        log_raise_400("move instruction mbid is not a valid mbid.")

    try:
        from_value = int(data["from"])
        to_value = int(data["to"])
        count_value = int(data["count"])
        if from_value < 0 or to_value < 0 or count_value < 0:
            raise ValueError
    except ValueError:
        log_raise_400("move instruction values for 'from', 'to' and 'count' must all be positive integers.")
Example #17
0
def load_release(release_mbid):
    """
    This endpoint takes a release mbid, loads the tracks for this release and makes a playlist from it and
    sends it to the front end via JSPF.

    :statuscode 200: playlist generated
    :statuscode 400: invalid recording_mbid arguments
    """

    release_mbid = release_mbid.strip()
    if not is_valid_uuid(release_mbid):
        raise BadRequest(f"Recording mbid {release_mbid} is not valid.")

    playlist = None
    if mb_engine:
        release = get_release_by_mbid(release_mbid, includes=["media", "artists"])
        if not release:
            raise NotFound("This release was not found in our database. It may not have replicated to this server yet.")

        name = "Release %s by %s" % (release["name"], release["artist-credit-phrase"])
        desc = 'Release <a href="https://musicbrainz.org/release/%s">%s</a> by %s' % (release["mbid"],
                                                                                      release["name"],
                                                                                      release["artist-credit-phrase"])
        now = datetime.now()
        playlist = WritablePlaylist(description=desc, name=name, creator="listenbrainz", creator_id=1, created=now)
        for medium in release["medium-list"]:
            for recording in medium["track-list"]:
                rec = WritablePlaylistRecording(title=recording["name"],
                                                artist_credit=release["artist-credit-phrase"],
                                                artist_mbids=[a["artist"]["mbid"] for a in recording["artist-credit"]],
                                                release_name=release["name"],
                                                release_mbid=release["mbid"],
                                                position=recording["position"],
                                                mbid=recording["recording_id"],
                                                added_by_id=1, created=now)
                playlist.recordings.append(rec)

    return render_template(
        "player/player-page.html",
        props=ujson.dumps({"playlist": serialize_jspf(playlist) if playlist is not None else {}})
    )
Example #18
0
def load_playlist(playlist_mbid: str):
    """Load a single playlist by id
    """
    if not is_valid_uuid(playlist_mbid):
        raise BadRequest("Provided playlist ID is invalid: %s" % playlist_mbid)

    current_user_id = None
    if current_user.is_authenticated:
        current_user_id = current_user.id

    playlist = db_playlist.get_by_mbid(playlist_mbid, True)
    if playlist is None or not playlist.is_visible_by(current_user_id):
        raise NotFound("Cannot find playlist: %s" % playlist_mbid)

    fetch_playlist_recording_metadata(playlist)

    spotify_data = {}
    current_user_data = {}
    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,
        }
    props = {
        "current_user": current_user_data,
        "spotify": spotify_data,
        "api_url": current_app.config["API_URL"],
        "labs_api_url": current_app.config["LISTENBRAINZ_LABS_API_URL"],
        "web_sockets_server_url": current_app.config['WEBSOCKETS_SERVER_URL'],
        "playlist": serialize_jspf(playlist),
        "sentry_dsn": current_app.config.get("LOG_SENTRY", {}).get("dsn")
    }

    return render_template(
        "playlists/playlist.html",
        props=ujson.dumps(props)
    )
Example #19
0
def load_playlist(playlist_mbid: str):
    """Load a single playlist by id
    """
    if not is_valid_uuid(playlist_mbid):
        raise BadRequest("Provided playlist ID is invalid: %s" % playlist_mbid)

    current_user_id = None
    if current_user.is_authenticated:
        current_user_id = current_user.id

    playlist = db_playlist.get_by_mbid(playlist_mbid, True)
    if playlist is None or not playlist.is_visible_by(current_user_id):
        raise NotFound("Cannot find playlist: %s" % playlist_mbid)

    fetch_playlist_recording_metadata(playlist)

    props = {
        "labs_api_url": current_app.config["LISTENBRAINZ_LABS_API_URL"],
        "web_sockets_server_url": current_app.config['WEBSOCKETS_SERVER_URL'],
        "playlist": serialize_jspf(playlist),
    }

    return render_template("playlists/playlist.html", props=ujson.dumps(props))
Example #20
0
    def test_get_listens(self):
        """ Test to make sure that the api sends valid listens on get requests.
        """
        with open(self.path_to_data_file('valid_single.json'), 'r') as f:
            payload = json.load(f)

        # send a listen
        ts = int(time.time())
        payload['payload'][0]['listened_at'] = ts
        response = self.send_data(payload)
        self.assert200(response)
        self.assertEqual(response.json['status'], 'ok')

        url = url_for('api_v1.get_listens',
                      user_name=self.user['musicbrainz_id'])
        response = self.wait_for_query_to_have_items(
            url, 1, query_string={'count': '1'})
        data = json.loads(response.data)['payload']

        self.assert200(response)

        # make sure user id is correct
        self.assertEqual(data['user_id'], self.user['musicbrainz_id'])

        # make sure that count is 1 and list also contains 1 listen
        self.assertEqual(data['count'], 1)
        self.assertEqual(len(data['listens']), 1)

        # make sure timestamp is the same as sent
        sent_time = payload['payload'][0]['listened_at']
        self.assertEqual(data['listens'][0]['listened_at'], sent_time)
        self.assertEqual(data['listens'][0]['track_metadata']['track_name'],
                         'Fade')
        self.assertEqual(data['listens'][0]['track_metadata']['artist_name'],
                         'Kanye West')
        self.assertEqual(data['listens'][0]['track_metadata']['release_name'],
                         'The Life of Pablo')
        self.assertEqual(
            data['listens'][0]['track_metadata']['additional_info']
            ['listening_from'], 'spotify')

        # make sure that artist msid, release msid and recording msid are present in data
        self.assertTrue(is_valid_uuid(data['listens'][0]['recording_msid']))
        self.assertTrue(
            is_valid_uuid(data['listens'][0]['track_metadata']
                          ['additional_info']['artist_msid']))
        self.assertTrue(
            is_valid_uuid(data['listens'][0]['track_metadata']
                          ['additional_info']['release_msid']))

        # check for latest listen timestamp
        self.assertEqual(data['latest_listen_ts'], ts)

        # request with min_ts should work
        response = self.client.get(url,
                                   query_string={'min_ts': int(time.time())})
        self.assert200(response)

        # request with max_ts lesser than the timestamp of the submitted listen
        # should not send back any listens, should report a good latest_listen timestamp
        response = self.client.get(url, query_string={'max_ts': ts - 2})
        self.assert200(response)
        self.assertListEqual(response.json['payload']['listens'], [])
        self.assertEqual(response.json['payload']['latest_listen_ts'], ts)

        # test request with both max_ts and min_ts is working
        url = url_for('api_v1.get_listens',
                      user_name=self.user['musicbrainz_id'])

        response = self.client.get(url,
                                   query_string={
                                       'max_ts': ts + 1000,
                                       'min_ts': ts - 1000
                                   })
        self.assert200(response)
        data = json.loads(response.data)['payload']

        self.assertEqual(data['user_id'], self.user['musicbrainz_id'])

        self.assertEqual(data['count'], 1)
        self.assertEqual(len(data['listens']), 1)

        sent_time = payload['payload'][0]['listened_at']
        self.assertEqual(data['listens'][0]['listened_at'], sent_time)
        self.assertEqual(data['listens'][0]['track_metadata']['track_name'],
                         'Fade')
        self.assertEqual(data['listens'][0]['track_metadata']['artist_name'],
                         'Kanye West')
        self.assertEqual(data['listens'][0]['track_metadata']['release_name'],
                         'The Life of Pablo')

        # check that recent listens are fetched correctly
        url = url_for('api_v1.get_recent_listens_for_user_list',
                      user_list=self.user['musicbrainz_id'])
        response = self.client.get(url, query_string={'limit': '1'})
        self.assert200(response)
        data = json.loads(response.data)['payload']
        self.assertEqual(data['count'], 1)

        url = url_for('api_v1.get_listen_count',
                      user_name=self.user['musicbrainz_id'])
        response = self.client.get(url)
        self.assert200(response)
        data = json.loads(response.data)['payload']
        self.assertEqual(data['count'], 1)

        url = url_for('api_v1.get_listen_count', user_name="sir_dumpsterfire")
        response = self.client.get(url)
        self.assert404(response)
Example #21
0
    def test_get_listens(self):
        """ Test to make sure that the api sends valid listens on get requests.
        """
        with open(self.path_to_data_file('valid_single.json'), 'r') as f:
            payload = json.load(f)

        # send a listen
        ts = int(time.time())
        payload['payload'][0]['listened_at'] = ts
        response = self.send_data(payload)
        self.assert200(response)
        self.assertEqual(response.json['status'], 'ok')

        # This sleep allows for the influx subscriber to take its time in getting
        # the listen submitted from redis and writing it to influx.
        # Removing it causes an empty list of listens to be returned.
        time.sleep(2)

        url = url_for('api_v1.get_listens',
                      user_name=self.user['musicbrainz_id'])
        response = self.client.get(url, query_string={'count': '1'})
        self.assert200(response)
        data = json.loads(response.data)['payload']

        # make sure user id is correct
        self.assertEqual(data['user_id'], self.user['musicbrainz_id'])

        # make sure that count is 1 and list also contains 1 listen
        self.assertEqual(data['count'], 1)
        self.assertEqual(len(data['listens']), 1)

        # make sure timestamp is the same as sent
        sent_time = payload['payload'][0]['listened_at']
        self.assertEqual(data['listens'][0]['listened_at'], sent_time)
        self.assertEqual(data['listens'][0]['track_metadata']['track_name'],
                         'Fade')
        self.assertEqual(data['listens'][0]['track_metadata']['artist_name'],
                         'Kanye West')
        self.assertEqual(data['listens'][0]['track_metadata']['release_name'],
                         'The Life of Pablo')

        # make sure that artist msid, release msid and recording msid are present in data
        self.assertTrue(is_valid_uuid(data['listens'][0]['recording_msid']))
        self.assertTrue(
            is_valid_uuid(data['listens'][0]['track_metadata']
                          ['additional_info']['artist_msid']))
        self.assertTrue(
            is_valid_uuid(data['listens'][0]['track_metadata']
                          ['additional_info']['release_msid']))

        # check for latest listen timestamp
        self.assertEqual(data['latest_listen_ts'], ts)

        # request with min_ts should work
        response = self.client.get(url,
                                   query_string={'min_ts': int(time.time())})
        self.assert200(response)

        # request with max_ts lesser than the timestamp of the submitted listen
        # should not send back any listens, should report a good latest_listen timestamp
        response = self.client.get(url, query_string={'max_ts': ts - 2})
        self.assert200(response)
        self.assertListEqual(response.json['payload']['listens'], [])
        self.assertEqual(response.json['payload']['latest_listen_ts'], ts)

        # checkt that recent listens are fectched correctly
        url = url_for('api_v1.get_recent_listens_for_user_list',
                      user_list=self.user['musicbrainz_id'])
        response = self.client.get(url, query_string={'count': '1'})
        self.assert200(response)
        data = json.loads(response.data)['payload']
        self.assertEqual(data['count'], 2)
def _to_native_api(data, append_key):
    """ Converts the scrobble submitted with given string appended to keys to the audioscrobbler api into
        a listen of the native api format. Returns None if no scrobble exists with that string appended
        to keys.

        Returns: dict of form
            {
                'listened_at': (int)
                'track_metadata': {
                    'artist_name': (str),
                    'track_name': (str)
                }
            }

    """

    try:
        listen = {
            'track_metadata': {
                'artist_name': data['a{}'.format(append_key)],
                'track_name': data['t{}'.format(append_key)],
                'release_name': data['b{}'.format(append_key)],
                'additional_info': {}
            }
        }
    except KeyError:
        return None

    # if this is not a now playing request, get the timestamp
    if append_key != '':
        try:
            listen['listened_at'] = int(data['i{}'.format(append_key)])
            validate_listened_at(listen)
        except (KeyError, ValueError, ListenValidationError):
            return None

    if 'o{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['source'] = data[
            'o{}'.format(append_key)]

    if 'r{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['rating'] = data[
            'r{}'.format(append_key)]

    if 'n{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['track_number'] = data[
            'n{}'.format(append_key)]

    if 'm{}'.format(append_key) in data:
        mbid = data['m{}'.format(append_key)]
        if is_valid_uuid(mbid):
            listen['track_metadata']['additional_info'][
                'recording_mbid'] = mbid

    if 'l{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['track_length'] = data[
            'l{}'.format(append_key)]

    # if there is nothing in the additional info field of the track, remove it
    if listen['track_metadata']['additional_info'] == {}:
        del listen['track_metadata']['additional_info']

    try:
        check_for_unicode_null_recursively(listen)
    except ListenValidationError:
        return None

    return listen
Example #23
0
def _to_native_api(data, append_key):
    """ Converts the scrobble submitted with given string appended to keys to the audioscrobbler api into
        a listen of the native api format. Returns None if no scrobble exists with that string appended
        to keys.

        Returns: dict of form
            {
                'listened_at': (int)
                'track_metadata': {
                    'artist_name': (str),
                    'track_name': (str)
                }
            }

    """

    try:
        listen = {
            'track_metadata': {
                'artist_name': data['a{}'.format(append_key)],
                'track_name': data['t{}'.format(append_key)],
                'release_name': data['b{}'.format(append_key)],
                'additional_info': {}
            }
        }
    except KeyError:
        return None

    # if this is not a now playing request, get the timestamp
    if append_key != '':
        try:
            listen['listened_at'] = int(data['i{}'.format(append_key)])
            validate_listened_at(listen)
        except (KeyError, ValueError, ListenValidationError):
            return None

    # Source has special meaning in LFM api. e.g.: if you see a listen with
    # "source": "P", in the database it is a listen from API compat deprecated.
    # https://web.archive.org/web/20090217162831/http://last.fm/api/submissions
    if 'o{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['source'] = data[
            'o{}'.format(append_key)]

    if 'r{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['rating'] = data[
            'r{}'.format(append_key)]

    if 'n{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['track_number'] = data[
            'n{}'.format(append_key)]

    if 'm{}'.format(append_key) in data:
        mbid = data['m{}'.format(append_key)]
        if is_valid_uuid(mbid):
            listen['track_metadata']['additional_info'][
                'recording_mbid'] = mbid

    if 'l{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['track_length'] = data[
            'l{}'.format(append_key)]

    # if there is nothing in the additional info field of the track, remove it
    if listen['track_metadata']['additional_info'] == {}:
        del listen['track_metadata']['additional_info']

    try:
        check_for_unicode_null_recursively(listen)
    except ListenValidationError:
        return None

    return listen
Example #24
0
def edit_playlist(playlist_mbid):
    """
    Edit the private/public status, name, description or list of collaborators for an exising playlist.
    The Authorization header must be set and correspond to the owner of the playlist otherwise a 403
    error will be returned. All fields will be overwritten with new values.

    :reqheader Authorization: Token <user token>
    :statuscode 200: playlist accepted.
    :statuscode 400: invalid JSON sent, see error message for details.
    :statuscode 401: invalid authorization. See error message for details.
    :statuscode 403: forbidden. The subitting user is not allowed to edit playlists for other users.
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()

    data = request.json
    validate_playlist(data)

    if not is_valid_uuid(playlist_mbid):
        log_raise_400("Provided playlist ID is invalid.")

    playlist = db_playlist.get_by_mbid(playlist_mbid, False)
    if playlist is None or not playlist.is_visible_by(user["id"]):
        raise APINotFound("Cannot find playlist: %s" % playlist_mbid)

    if playlist.creator_id != user["id"]:
        raise APIForbidden("You are not allowed to edit this playlist.")

    try:
        playlist.public = data["playlist"]["extension"][PLAYLIST_EXTENSION_URI]["public"]
    except KeyError:
        pass

    if "annotation" in data["playlist"]:
        # If the annotation key exists but the value is empty ("" or None),
        # unset the description
        description = data["playlist"]["annotation"]
        if description:
            description = _filter_description_html(description)
        else:
            description = None
        playlist.description = description

    if data["playlist"].get("title"):
        playlist.name = data["playlist"]["title"]

    collaborators = data.get("playlist", {}).\
        get("extension", {}).get(PLAYLIST_EXTENSION_URI, {}).\
        get("collaborators", [])
    users = {}

    # Uniquify collaborators list
    collaborators = list(set(collaborators))

    # Don't allow creator to also be a collaborator
    if user["musicbrainz_id"] in collaborators:
        collaborators.remove(user["musicbrainz_id"])

    if collaborators:
        users = db_user.get_many_users_by_mb_id(collaborators)

    collaborator_ids = []
    for collaborator in collaborators:
        if collaborator.lower() not in users:
            log_raise_400("Collaborator {} doesn't exist".format(collaborator))
        collaborator_ids.append(users[collaborator.lower()]["id"])

    playlist.collaborators = collaborators
    playlist.collaborator_ids = collaborator_ids

    db_playlist.update_playlist(playlist)

    return jsonify({'status': 'ok'})
Example #25
0
 def test_valid_uuid(self):
     self.assertTrue(is_valid_uuid(str(uuid.uuid4())))
     self.assertFalse(is_valid_uuid('hjjkghjk'))
     self.assertFalse(is_valid_uuid(123))
def _to_native_api(data, append_key):
    """ Converts the scrobble submitted with given string appended to keys to the audioscrobbler api into
        a listen of the native api format. Returns None if no scrobble exists with that string appended
        to keys.

        Returns: dict of form
            {
                'listened_at': (int)
                'track_metadata': {
                    'artist_name': (str),
                    'track_name': (str)
                }
            }

    """

    try:
        listen = {
            'track_metadata': {
                'artist_name': data['a{}'.format(append_key)],
                'track_name': data['t{}'.format(append_key)],
                'release_name': data['b{}'.format(append_key)],
                'additional_info': {}
            }
        }
    except KeyError:
        return None

    # if this is not a now playing request, get the timestamp
    if append_key != '':
        try:
            listen['listened_at'] = int(data['i{}'.format(append_key)])
        except (KeyError, ValueError):
            return None

        # if timestamp is too high, this is an invalid listen
        # in order to make up for possible clock skew, we allow
        # timestamps to be one hour ahead of server time
        if not is_valid_timestamp(listen['listened_at']):
            return None

    if 'o{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['source'] = data[
            'o{}'.format(append_key)]

    if 'r{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['rating'] = data[
            'r{}'.format(append_key)]

    if 'n{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['track_number'] = data[
            'n{}'.format(append_key)]

    if 'm{}'.format(append_key) in data:
        mbid = data['m{}'.format(append_key)]
        if is_valid_uuid(mbid):
            listen['track_metadata']['additional_info'][
                'recording_mbid'] = mbid

    if 'l{}'.format(append_key) in data:
        listen['track_metadata']['additional_info']['track_length'] = data[
            'l{}'.format(append_key)]

    # if there is nothing in the additional info field of the track, remove it
    if listen['track_metadata']['additional_info'] == {}:
        del listen['track_metadata']['additional_info']

    return listen