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))
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))
def get_similar_to_user(user_name, other_user_name): """ Get the similarity of the user and the other user, based on their listening history. Returns a single dict: { "user_name": "other_user", "similarity": 0.1938480256 } :param user_name: the MusicBrainz ID of the the one user :param other_user_name: the MusicBrainz ID of the other user whose similar users are :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* :statuscode 404: The requested user was not found. """ user = db_user.get_by_mb_id(user_name) if not user: raise APINotFound("User %s not found" % user_name) similar_users = db_user.get_similar_users(user['id']) try: return jsonify({ 'payload': { "user_name": other_user_name, "similarity": similar_users.similar_users[other_user_name] } }) except KeyError: raise APINotFound("Similar-to user not found")
def follow_user(user_name: str): """ Follow the user ``user_name``. A user token (found on https://listenbrainz.org/profile/ ) must be provided in the Authorization header! :reqheader Authorization: Token <user token> :reqheader Content-Type: *application/json* :statuscode 200: Successfully followed the user ``user_name``. :statuscode 400: - Already following the user ``user_name``. - Trying to follow yourself. :statuscode 401: invalid authorization. See error message for details. :resheader Content-Type: *application/json* """ current_user = validate_auth_header() user = db_user.get_by_mb_id(user_name) if not user: raise APINotFound("User %s not found" % user_name) if user["musicbrainz_id"] == current_user["musicbrainz_id"]: raise APIBadRequest("Whoops, cannot follow yourself.") if db_user_relationship.is_following_user(current_user["id"], user["id"]): raise APIBadRequest("%s is already following user %s" % (current_user["musicbrainz_id"], user["musicbrainz_id"])) try: db_user_relationship.insert(current_user["id"], user["id"], "follow") except Exception as e: current_app.logger.error("Error while trying to insert a relationship: %s", str(e)) raise APIInternalServerError("Something went wrong, please try again later") return jsonify({"status": "ok"})
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 unpin_recording_for_user(): """ Unpins the currently active pinned recording for the user. A user token (found on https://listenbrainz.org/profile/) must be provided in the Authorization header! :reqheader Authorization: Token <user token> :statuscode 200: recording unpinned. :statuscode 401: invalid authorization. See error message for details. :statuscode 404: could not find the active recording to unpin for the user. See error message for details. :resheader Content-Type: *application/json* """ user = validate_auth_header() try: recording_unpinned = db_pinned_rec.unpin(user["id"]) except Exception as e: current_app.logger.error( "Error while unpinning recording for user: {}".format(e)) raise APIInternalServerError("Something went wrong. Please try again.") if recording_unpinned is False: raise APINotFound( "Cannot find an active pinned recording for user '%s' to unpin" % (user["musicbrainz_id"])) return jsonify({"status": "ok"})
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_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! :statuscode 404: The requested user was not found. :resheader Content-Type: *application/json* """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: %s" % user_name) try: listen_count = timescale_connection._ts.get_listen_count_for_user(user["id"]) 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 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})
def get_playlists_created_for_user(playlist_user_name): """ Fetch playlist metadata in JSPF format without recordings that have been created for the user. Createdfor playlists are all public, so no Authorization is needed for this call. :params count: The number of playlists to return (for pagination). Default :data:`~webserver.views.api.DEFAULT_NUMBER_OF_PLAYLISTS_PER_CALL` :params offset: The offset of into the list of playlists to return (for pagination) :statuscode 200: Yay, you have data! :statuscode 404: User not found :resheader Content-Type: *application/json* """ count = get_non_negative_param('count', DEFAULT_NUMBER_OF_PLAYLISTS_PER_CALL) offset = get_non_negative_param('offset', 0) playlist_user = db_user.get_by_mb_id(playlist_user_name) if playlist_user is None: raise APINotFound("Cannot find user: %s" % playlist_user_name) playlists, playlist_count = db_playlist.get_playlists_created_for_user( playlist_user["id"], load_recordings=False, count=count, offset=offset) return jsonify( serialize_playlists(playlists, playlist_count, count, offset))
def get_playlists_for_user(playlist_user_name): """ Fetch playlist metadata in JSPF format without recordings for the given user. If a user token is provided in the Authorization header, return private playlists as well as public playlists for that user. :params count: The number of playlists to return (for pagination). Default :data:`~webserver.views.api.DEFAULT_NUMBER_OF_PLAYLISTS_PER_CALL` :params offset: The offset of into the list of playlists to return (for pagination) :statuscode 200: Yay, you have data! :statuscode 404: User not found :resheader Content-Type: *application/json* """ user = validate_auth_header(True) count = get_non_negative_param('count', DEFAULT_NUMBER_OF_PLAYLISTS_PER_CALL) offset = get_non_negative_param('offset', 0) playlist_user = db_user.get_by_mb_id(playlist_user_name) if playlist_user is None: raise APINotFound("Cannot find user: %s" % playlist_user_name) include_private = True if user and user["id"] == playlist_user[ "id"] else False playlists, playlist_count = db_playlist.get_playlists_for_user( playlist_user["id"], include_private=include_private, load_recordings=False, count=count, offset=offset) return jsonify( serialize_playlists(playlists, playlist_count, count, offset))
def get_following(user_name: str): """ Fetch the list of users followed by the user ``user_name``. Returns a JSON with an array of user names like these: .. code-block:: json { "followers": ["rob", "mr_monkey", "..."], "user": "******" } :statuscode 200: Yay, you have data! :statuscode 404: User not found """ user = db_user.get_by_mb_id(user_name) if not user: raise APINotFound("User %s not found" % user_name) try: following = db_user_relationship.get_following_for_user(user["id"]) following = [user["musicbrainz_id"] for user in following] except Exception as e: current_app.logger.error("Error while trying to fetch following: %s", str(e)) raise APIInternalServerError( "Something went wrong, please try again later") return jsonify({"following": following, "user": user["musicbrainz_id"]})
def get_playlists_collaborated_on_for_user(playlist_user_name): """ Fetch playlist metadata in JSPF format without recordings for which a user is a collaborator. If a playlist is private, it will only be returned if the caller is authorized to edit that playlist. :params count: The number of playlists to return (for pagination). Default :data:`~webserver.views.api.DEFAULT_NUMBER_OF_PLAYLISTS_PER_CALL` :params offset: The offset of into the list of playlists to return (for pagination) :statuscode 200: Yay, you have data! :statuscode 404: User not found :resheader Content-Type: *application/json* """ user = validate_auth_header(True) count = get_non_negative_param('count', DEFAULT_NUMBER_OF_PLAYLISTS_PER_CALL) offset = get_non_negative_param('offset', 0) playlist_user = db_user.get_by_mb_id(playlist_user_name) if playlist_user is None: raise APINotFound("Cannot find user: %s" % playlist_user_name) # TODO: This needs to be passed to the DB layer include_private = True if user and user["id"] == playlist_user[ "id"] else False playlists, playlist_count = db_playlist.get_playlists_collaborated_on( playlist_user["id"], include_private=include_private, load_recordings=False, count=count, offset=offset) return jsonify( serialize_playlists(playlists, playlist_count, count, offset))
def unfollow_user(user_name: str): """ Unfollow the user ``user_name``. A user token (found on https://listenbrainz.org/profile/ ) must be provided in the Authorization header! :reqheader Authorization: Token <user token> :reqheader Content-Type: *application/json* :statuscode 200: Successfully unfollowed the user ``user_name``. :statuscode 401: invalid authorization. See error message for details. :resheader Content-Type: *application/json* """ current_user = validate_auth_header() user = db_user.get_by_mb_id(user_name) if not user: raise APINotFound("User %s not found" % user_name) try: db_user_relationship.delete(current_user["id"], user["id"], "follow") except Exception as e: current_app.logger.error( "Error while trying to delete a relationship: %s", str(e)) raise APIInternalServerError( "Something went wrong, please try again later") return jsonify({"status": "ok"})
def get_playing_now(user_name): """ Get the listen being played right now for user ``user_name``. This endpoint returns a JSON document with a single listen in the same format as the ``/user/<user_name>/listens`` endpoint, with one key difference, there will only be one listen returned at maximum and the listen will not contain a ``listened_at`` element. The format for the JSON returned is defined in our :ref:`json-doc`. :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: %s" % user_name) playing_now_listen = redis_connection._redis.get_playing_now(user['id']) listen_data = [] count = 0 if playing_now_listen: count += 1 listen_data = [{ 'track_metadata': playing_now_listen.data, }] return jsonify({ 'payload': { 'count': count, 'user_id': user_name, 'playing_now': True, 'listens': listen_data, }, })
def delete_pin_for_user(row_id): """ Deletes the pinned recording with given ``row_id`` from the server. A user token (found on https://listenbrainz.org/profile/) must be provided in the Authorization header! :reqheader Authorization: Token <user token> :param row_id: the row_id of the pinned recording that should be deleted. :type row_id: ``int`` :statuscode 200: recording unpinned. :statuscode 401: invalid authorization. See error message for details. :statuscode 404: the requested row_id for the user was not found. :resheader Content-Type: *application/json* """ user = validate_auth_header() try: recording_deleted = db_pinned_rec.delete(row_id, user["id"]) except Exception as e: current_app.logger.error( "Error while unpinning recording for user: {}".format(e)) raise APIInternalServerError("Something went wrong. Please try again.") if recording_deleted is False: raise APINotFound("Cannot find pin with row_id '%s' for user '%s'" % (row_id, user["musicbrainz_id"])) return jsonify({"status": "ok"})
def get_similar_users(user_name): """ Get list of users who have similar music tastes (based on their listen history) for a given user. Returns an array of dicts like these: { "user_name": "hwnrwx", "similarity": 0.1938480256 } :param user_name: the MusicBrainz ID of the user whose similar users are being requested. :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* :statuscode 404: The requested user was not found. """ user = db_user.get_by_mb_id(user_name) if not user: raise APINotFound("User %s not found" % user_name) similar_users = db_user.get_similar_users(user['id']) response = [] for user_name in similar_users.similar_users: response.append({ 'user_name': user_name, 'similarity': similar_users.similar_users[user_name] }) return jsonify({ 'payload': sorted(response, key=itemgetter('similarity'), reverse=True) })
def delete_feed_events(user_name): ''' Delete those events from user's feed that belong to them. Supports deletion of recommendation and notification. Along with the authorization token, post the event type and event id. For example: .. code-block:: json { "event_type": "recording_recommendation", "id": "<integer id of the event>" } .. code-block:: json { "event_type": "notification", "id": "<integer id of the event>" } :param user_name: The MusicBrainz ID of the user from whose timeline events are being deleted :type user_name: ``str`` :statuscode 200: Successful deletion :statuscode 400: Bad request, check ``response['error']`` for more details. :statuscode 401: Unauthorized :statuscode 404: User not found :statuscode 500: API Internal Server Error :resheader Content-Type: *application/json* ''' user = validate_auth_header() if user_name != user['musicbrainz_id']: raise APIUnauthorized( "You don't have permissions to delete from this user's timeline.") try: event = ujson.loads(request.get_data()) if event["event_type"] in [ UserTimelineEventType.RECORDING_RECOMMENDATION.value, UserTimelineEventType.NOTIFICATION.value ]: try: event_deleted = db_user_timeline_event.delete_user_timeline_event( event["id"], user["id"]) except Exception as e: raise APIInternalServerError( "Something went wrong. Please try again") if not event_deleted: raise APINotFound( "Cannot find '%s' event with id '%s' for user '%s'" % (event["event_type"], event["id"], user["id"])) return jsonify({"status": "ok"}) raise APIBadRequest( "This event type is not supported for deletion via this method") except (ValueError, KeyError) as e: raise APIBadRequest(f"Invalid JSON: {str(e)}")
def latest_import(): """ Get and update the timestamp of the newest listen submitted in previous imports to ListenBrainz. In order to get the timestamp for a user, make a GET request to this endpoint. The data returned will be JSON of the following format: .. code-block:: json { "musicbrainz_id": "the MusicBrainz ID of the user", "latest_import": "the timestamp of the newest listen submitted in previous imports. Defaults to 0" } :param user_name: the MusicBrainz ID of the user whose data is needed :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* In order to update the timestamp of a user, you'll have to provide a user token in the Authorization Header. User tokens can be found on https://listenbrainz.org/profile/ . The JSON that needs to be posted must contain a field named `ts` in the root with a valid unix timestamp. :reqheader Authorization: Token <user token> :reqheader Content-Type: *application/json* :statuscode 200: latest import timestamp updated :statuscode 400: invalid JSON sent, see error message for details. :statuscode 401: invalid authorization. See error message for details. """ if request.method == 'GET': user_name = request.args.get('user_name', '') user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound( "Cannot find user: {user_name}".format(user_name=user_name)) return jsonify({ 'musicbrainz_id': user['musicbrainz_id'], 'latest_import': 0 if not user['latest_import'] else int( user['latest_import'].strftime('%s')) }) elif request.method == 'POST': user = validate_auth_header() try: ts = ujson.loads(request.get_data()).get('ts', 0) except ValueError: raise APIBadRequest('Invalid data sent') try: db_user.increase_latest_import(user['musicbrainz_id'], int(ts)) except DatabaseException as e: current_app.logger.error( "Error while updating latest import: {}".format(e)) raise APIInternalServerError( 'Could not update latest_import, try again') return jsonify({'status': 'ok'})
def get_feedback_for_recordings_for_user(user_name): """ Get feedback given by user ``user_name`` for the list of recordings supplied. A sample response may look like: .. code-block:: json { "feedback": [ { "created": 1604033691, "rating": "bad_recommendation", "recording_mbid": "9ffabbe4-e078-4906-80a7-3a02b537e251" }, { "created": 1604032934, "rating": "hate", "recording_mbid": "28111d2c-a80d-418f-8b77-6aba58abe3e7" } ], "user_name": "Vansika Pareek" } An empty response will be returned if the feedback for given recording MBID doesn't exist. :param mbids: comma separated list of recording_mbids for which feedback records are to be fetched. :type mbids: ``str`` :statuscode 200: Yay, you have data! :statuscode 400: Bad request, check ``response['error']`` for more details. :statuscode 404: User not found. :resheader Content-Type: *application/json* """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: %s" % user_name) mbids = request.args.get('mbids') if not mbids: raise APIBadRequest("Please provide comma separated recording 'mbids'!") recording_list = parse_recording_mbid_list(mbids) if not len(recording_list): raise APIBadRequest("Please provide comma separated recording mbids!") try: feedback = db_feedback.get_feedback_for_multiple_recordings_for_user(user_id=user["id"], recording_list=recording_list) except ValidationError as e: log_raise_400("Invalid JSON document submitted: %s" % str(e).replace("\n ", ":").replace("\n", " "), request.args) recommendation_feedback = [_format_feedback(fb) for fb in feedback] return jsonify({ "feedback": recommendation_feedback, "user_name": user_name })
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'})
def _validate_stats_user_params(user_name) -> Tuple[Dict, str]: """ Validate and return the user and common stats params """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound(f"Cannot find user: {user_name}") stats_range = request.args.get("range", default="all_time") if not _is_valid_range(stats_range): raise APIBadRequest(f"Invalid range: {stats_range}") return user, stats_range
def refresh_service_token(service_name: str): service = _get_service_or_raise_404(service_name) user = service.get_user(current_user.id) if not user: raise APINotFound("User has not authenticated to %s" % service_name.capitalize()) if service.user_oauth_token_has_expired(user): try: user = service.refresh_access_token(current_user.id, user["refresh_token"]) except ExternalServiceInvalidGrantError: raise APINotFound("User has revoked authorization to %s" % service_name.capitalize()) except Exception: raise APIServiceUnavailable("Cannot refresh %s token right now" % service_name.capitalize()) return jsonify({"access_token": user["access_token"]})
def get_dump_info(): """ Get information about ListenBrainz data dumps. You need to pass the `id` parameter in a GET request to get data about that particular dump. **Example response**: .. code-block:: json { "id": 1, "timestamp": "20190625-165900" } :query id: Integer specifying the ID of the dump, if not provided, the endpoint returns information about the latest data dump. :statuscode 200: You have data. :statuscode 400: You did not provide a valid dump ID. See error message for details. :statuscode 404: Dump with given ID does not exist. :resheader Content-Type: *application/json* """ dump_id = request.args.get("id") if dump_id is None: try: dump = db_dump.get_dump_entries()[0] # return the latest dump except IndexError: raise APINotFound("No dump entry exists.") else: try: dump_id = int(dump_id) except ValueError: raise APIBadRequest("The `id` parameter needs to be an integer.") dump = db_dump.get_dump_entry(dump_id) if dump is None: raise APINotFound("No dump exists with ID: %d" % dump_id) return jsonify({ "id": dump["id"], "timestamp": _convert_timestamp_to_string_dump_format(dump["created"]), })
def year_in_music(user_name: str): """ Get data for year in music stuff """ user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound(f"Cannot find user: {user_name}") return jsonify({ "payload": { "user_name": user_name, "data": get_year_in_music(user["id"]) or {} } })
def refresh_spotify_token(): spotify_user = spotify.get_user(current_user.id) if not spotify_user: raise APINotFound("User has not authenticated to Spotify") if spotify_user.token_expired: try: spotify_user = spotify.refresh_user_token(spotify_user) except spotify.SpotifyAPIError: raise APIServiceUnavailable( "Cannot refresh Spotify token right now") except spotify.SpotifyInvalidGrantError: raise APINotFound("User has revoked authorization to Spotify") return jsonify({ 'id': current_user.id, 'musicbrainz_id': current_user.musicbrainz_id, 'user_token': spotify_user.user_token, 'permission': spotify_user.permission, })
def get_feedback_for_user(user_name): """ Get feedback given by user ``user_name``. The format for the JSON returned is defined in our :ref:`feedback-json-doc`. If the optional argument ``score`` is not given, this endpoint will return all the feedback submitted by the user. Otherwise filters the feedback to be returned by score. :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`` :param metadata: Optional, 'true' or 'false' if this call should return the metadata for the feedback. :type metadata: ``str`` :statuscode 200: Yay, you have data! :resheader Content-Type: *application/json* """ score = _parse_int_arg('score') metadata = parse_boolean_arg('metadata') 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) user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound("Cannot find user: %s" % user_name) 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_user(user_id=user["id"], limit=count, offset=offset, score=score, metadata=metadata) total_count = db_feedback.get_feedback_count_for_user(user["id"]) feedback = [fb.to_api() for fb in feedback] return jsonify({ "feedback": feedback, "count": len(feedback), "total_count": total_count, "offset": offset })
def create_user_notification_event(user_name): """ Post a message with a link on a user's timeline. Only approved users are allowed to perform this action. The request should contain the following data: .. code-block:: json { "metadata": { "message": <the message to post, required>, } } :param user_name: The MusicBrainz ID of the user on whose timeline the message is to be posted. :type user_name: ``str`` :statuscode 200: Successful query, message has been posted! :statuscode 400: Bad request, check ``response['error']`` for more details. :statuscode 403: Forbidden, you are not an approved user. :statuscode 404: User not found :resheader Content-Type: *application/json* """ creator = validate_auth_header() if creator["musicbrainz_id"] not in current_app.config[ 'APPROVED_PLAYLIST_BOTS']: raise APIForbidden( "Only approved users are allowed to post a message on a user's timeline." ) user = db_user.get_by_mb_id(user_name) if user is None: raise APINotFound(f"Cannot find user: {user_name}") try: data = ujson.loads(request.get_data())['metadata'] except (ValueError, KeyError) as e: raise APIBadRequest(f"Invalid JSON: {str(e)}") if "message" not in data: raise APIBadRequest("Invalid metadata: message is missing") message = _filter_description_html(data["message"]) metadata = NotificationMetadata(creator=creator['musicbrainz_id'], message=message) try: db_user_timeline_event.create_user_notification_event( user['id'], metadata) except DatabaseException: raise APIInternalServerError("Something went wrong, please try again.") return jsonify({'status': 'ok'})
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 save_list(): creator = _validate_auth_header() raw_data = request.get_data() try: data = ujson.loads(raw_data.decode("utf-8")) except ValueError as e: log_raise_400("Cannot parse JSON document: %s" % str(e), raw_data) try: list_name = data['name'] list_id = data['id'] members = data['users'] except KeyError as e: log_raise_400("JSON missing key: %s" % str(e)) members = db_user.validate_usernames(members) if list_id is None: # create a new list try: list_id = db_follow_list.save( name=list_name, creator=creator['id'], members=[member['id'] for member in members], ) except DatabaseException as e: raise APIForbidden("List with same name already exists.") else: # do some validation current_list = db_follow_list.get(list_id) if current_list is None: raise APINotFound("List not found: %d" % list_id) if current_list['creator'] != creator['id']: raise APIUnauthorized("You can only edit your own lists.") # update the old list db_follow_list.update( list_id=list_id, name=list_name, members=[member['id'] for member in members], ) return jsonify({ "code": 200, "message": "it worked!", "list_id": list_id, })