Mixonomer/music/api/api.py

316 lines
11 KiB
Python
Raw Normal View History

from flask import Blueprint, request, jsonify
2020-04-30 14:54:05 +01:00
from google.cloud import firestore
from werkzeug.security import generate_password_hash
2019-08-12 00:34:04 +01:00
import os
import json
import logging
from datetime import datetime
2020-07-29 10:45:40 +01:00
from music.api.decorators import login_required, login_or_basic_auth, \
admin_required, gae_cron, cloud_task, validate_json, validate_args
from music.cloud import queue_run_user_playlist, offload_or_run_user_playlist
from music.cloud.tasks import update_all_user_playlists, update_playlists
2020-07-29 10:45:40 +01:00
from music.tasks.create_playlist import create_playlist
from music.tasks.run_user_playlist import run_user_playlist
2020-04-30 14:54:05 +01:00
from music.model.user import User
from music.model.playlist import Playlist
2019-10-19 17:14:11 +01:00
import music.db.database as database
from spotframework.net.network import SpotifyNetworkException
blueprint = Blueprint('api', __name__)
db = firestore.Client()
logger = logging.getLogger(__name__)
@blueprint.route('/playlists', methods=['GET'])
@login_or_basic_auth
def all_playlists_route(user=None):
2020-04-30 14:54:05 +01:00
assert user is not None
2019-10-23 14:44:17 +01:00
return jsonify({
2020-07-29 10:45:40 +01:00
'playlists': [i.to_dict() for i in Playlist.collection.parent(user.key).fetch()]
2019-10-23 14:44:17 +01:00
}), 200
2019-07-31 20:31:01 +01:00
2020-07-29 10:45:40 +01:00
@blueprint.route('/playlist', methods=['GET', 'DELETE'])
@login_or_basic_auth
2020-07-29 10:45:40 +01:00
@validate_args(('name', str))
def playlist_get_delete_route(user=None):
2020-07-29 10:45:40 +01:00
playlist = Playlist.collection.parent(user.key).filter('name', '==', request.args['name']).get()
2020-07-29 10:45:40 +01:00
if playlist is None:
return jsonify({'error': f'playlist {request.args["name"]} not found'}), 404
2020-07-29 10:45:40 +01:00
if request.method == "GET":
return jsonify(playlist.to_dict()), 200
2020-07-29 10:45:40 +01:00
elif request.method == 'DELETE':
Playlist.collection.parent(user.key).delete(key=playlist.key)
return jsonify({"message": 'playlist deleted', "status": "success"}), 200
2020-07-29 10:45:40 +01:00
@blueprint.route('/playlist', methods=['POST', 'PUT'])
@login_or_basic_auth
@validate_json(('name', str))
def playlist_post_put_route(user=None):
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
request_json = request.get_json()
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
playlist_name = request_json['name']
playlist_references = []
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
if request_json.get('playlist_references', None):
if request_json['playlist_references'] != -1:
for i in request_json['playlist_references']:
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
playlist = Playlist.collection.parent(user.key).filter('name', '==', i).get()
if playlist is not None:
playlist_references.append(db.document(playlist.key))
else:
return jsonify({"message": f'managed playlist {i} not found', "status": "error"}), 400
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
if len(playlist_references) == 0 and request_json.get('playlist_references', None) != -1:
playlist_references = None
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
searched_playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get()
2020-04-30 14:54:05 +01:00
2020-07-29 10:45:40 +01:00
# CREATE
if request.method == 'PUT':
2020-07-29 10:45:40 +01:00
if searched_playlist is not None:
return jsonify({'error': 'playlist already exists'}), 400
2020-07-29 10:45:40 +01:00
playlist = Playlist(parent=user.key)
2020-07-29 10:45:40 +01:00
playlist.name = request_json['name']
2020-07-29 10:45:40 +01:00
for key in [i for i in Playlist.mutable_keys if i not in ['playlist_references', 'type']]:
setattr(playlist, key, request_json.get(key, None))
2019-08-05 21:43:09 +01:00
2020-07-29 10:45:40 +01:00
playlist.playlist_references = playlist_references
2020-07-29 10:45:40 +01:00
playlist.last_updated = datetime.utcnow()
playlist.lastfm_stat_last_refresh = datetime.utcnow()
2020-07-29 10:45:40 +01:00
if request_json.get('type'):
playlist_type = request_json['type'].strip().lower()
if playlist_type in ['default', 'recents', 'fmchart']:
playlist.type = playlist_type
else:
playlist.type = 'default'
logger.warning(f'invalid type ({playlist_type}), {user.username} / {playlist_name}')
2020-07-29 10:45:40 +01:00
if user.spotify_linked:
new_playlist = create_playlist(user, playlist_name)
playlist.uri = str(new_playlist.uri)
2020-07-29 10:45:40 +01:00
playlist.save()
logger.info(f'added {user.username} / {playlist_name}')
2020-07-29 10:45:40 +01:00
return jsonify({"message": 'playlist added', "status": "success"}), 201
2020-07-29 10:45:40 +01:00
# UPDATE
elif request.method == 'POST':
2020-07-29 10:45:40 +01:00
if searched_playlist is None:
return jsonify({'error': "playlist doesn't exist"}), 400
2020-07-29 10:45:40 +01:00
playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get()
2019-08-03 21:35:08 +01:00
2020-07-29 10:45:40 +01:00
# ATTRIBUTES
for rec_key, rec_item in request_json.items():
# type and parts require extra validation
if rec_key in [k for k in Playlist.mutable_keys if k not in ['type', 'parts', 'playlist_references']]:
setattr(playlist, rec_key, request_json[rec_key])
2019-08-03 21:35:08 +01:00
2020-07-29 10:45:40 +01:00
# COMPONENTS
if request_json.get('parts'):
if request_json['parts'] == -1:
playlist.parts = []
else:
playlist.parts = request_json['parts']
2020-07-29 10:45:40 +01:00
if playlist_references is not None:
if playlist_references == -1:
playlist.playlist_references = []
else:
playlist.playlist_references = playlist_references
2020-07-29 10:45:40 +01:00
# ATTRIBUTE WITH CHECKS
if request_json.get('type'):
playlist_type = request_json['type'].strip().lower()
if playlist_type in ['default', 'recents', 'fmchart']:
playlist.type = playlist_type
2019-08-03 21:35:08 +01:00
2020-07-29 10:45:40 +01:00
playlist.update()
logger.info(f'updated {user.username} / {playlist_name}')
2019-08-03 21:35:08 +01:00
2020-07-29 10:45:40 +01:00
return jsonify({"message": 'playlist updated', "status": "success"}), 200
2019-08-03 21:35:08 +01:00
@blueprint.route('/user', methods=['GET', 'POST'])
@login_or_basic_auth
2020-04-30 14:54:05 +01:00
def user_route(user=None):
assert user is not None
2019-08-03 21:35:08 +01:00
if request.method == 'GET':
2020-04-30 14:54:05 +01:00
return jsonify(user.to_dict()), 200
2019-08-05 21:43:09 +01:00
2020-04-30 14:54:05 +01:00
else: # POST
request_json = request.get_json()
2019-08-03 21:35:08 +01:00
if 'username' in request_json:
2020-04-30 14:54:05 +01:00
if request_json['username'].strip().lower() != user.username:
if user.type != "admin":
return jsonify({'status': 'error', 'message': 'unauthorized'}), 401
2019-08-03 21:35:08 +01:00
2020-04-30 14:54:05 +01:00
user = User.collection.filter('username', '==', request_json['username'].strip().lower()).get()
2019-08-03 21:35:08 +01:00
if 'locked' in request_json:
2020-04-30 14:54:05 +01:00
if user.type == "admin":
logger.info(f'updating lock {user.username} / {request_json["locked"]}')
user.locked = request_json['locked']
2019-08-03 21:35:08 +01:00
if 'spotify_linked' in request_json:
2020-04-30 14:54:05 +01:00
logger.info(f'deauthing {user.username}')
if request_json['spotify_linked'] is False:
2020-04-30 14:54:05 +01:00
user.access_token = None
user.refresh_token = None
user.spotify_linked = False
if 'lastfm_username' in request_json:
2020-04-30 14:54:05 +01:00
logger.info(f'updating lastfm username {user.username} -> {request_json["lastfm_username"]}')
user.lastfm_username = request_json['lastfm_username']
user.update()
2019-08-03 21:35:08 +01:00
2020-04-30 14:54:05 +01:00
logger.info(f'updated {user.username}')
2019-08-03 21:35:08 +01:00
return jsonify({'message': 'account updated', 'status': 'succeeded'}), 200
2019-08-03 21:35:08 +01:00
@blueprint.route('/users', methods=['GET'])
@login_or_basic_auth
@admin_required
def all_users_route(user=None):
2019-10-23 14:44:17 +01:00
return jsonify({
2020-04-30 14:54:05 +01:00
'accounts': [i.to_dict() for i in User.collection.fetch()]
2019-10-23 14:44:17 +01:00
}), 200
@blueprint.route('/user/password', methods=['POST'])
@login_required
2020-07-29 10:45:40 +01:00
@validate_json(('new_password', str), ('current_password', str))
2020-04-30 14:54:05 +01:00
def change_password(user=None):
request_json = request.get_json()
2020-07-29 10:45:40 +01:00
if len(request_json['new_password']) == 0:
return jsonify({"error": 'zero length password'}), 400
2020-07-29 10:45:40 +01:00
if len(request_json['new_password']) > 30:
return jsonify({"error": 'password too long'}), 400
2020-07-29 10:45:40 +01:00
if user.check_password(request_json['current_password']):
user.password = generate_password_hash(request_json['new_password'])
user.update()
logger.info(f'password udpated {user.username}')
2020-07-29 10:45:40 +01:00
return jsonify({"message": 'password changed', "status": "success"}), 200
else:
2020-07-29 10:45:40 +01:00
logger.warning(f"incorrect password {user.username}")
return jsonify({'error': 'wrong password provided'}), 401
@blueprint.route('/playlist/run', methods=['GET'])
@login_or_basic_auth
2020-07-29 10:45:40 +01:00
@validate_args(('name', str))
2020-04-30 14:54:05 +01:00
def run_playlist(user=None):
2020-07-29 10:45:40 +01:00
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
queue_run_user_playlist(user.username, request.args['name']) # pass to either cloud tasks or functions
else:
2020-07-29 10:45:40 +01:00
run_user_playlist(user.username, request.args['name']) # update synchronously
return jsonify({'message': 'execution requested', 'status': 'success'}), 200
@blueprint.route('/playlist/run/task', methods=['POST'])
@cloud_task
def run_playlist_task(): # receives cloud tasks request for update
payload = request.get_data(as_text=True)
if payload:
payload = json.loads(payload)
logger.info(f'running {payload["username"]} / {payload["name"]}')
offload_or_run_user_playlist(payload['username'], payload['name']) # check whether offloading to cloud function
return jsonify({'message': 'executed playlist', 'status': 'success'}), 200
logger.critical('no payload provided')
@blueprint.route('/playlist/run/user', methods=['GET'])
@login_or_basic_auth
2020-04-30 14:54:05 +01:00
def run_user(user=None):
2020-04-30 14:54:05 +01:00
if user.type == 'admin':
user_name = request.args.get('username', user.username)
else:
2020-04-30 14:54:05 +01:00
user_name = user.username
update_playlists(user_name)
return jsonify({'message': 'executed user', 'status': 'success'}), 200
@blueprint.route('/playlist/run/user/task', methods=['POST'])
@cloud_task
def run_user_task():
payload = request.get_data(as_text=True)
if payload:
update_playlists(payload)
return jsonify({'message': 'executed user', 'status': 'success'}), 200
@blueprint.route('/playlist/run/users', methods=['GET'])
@login_or_basic_auth
@admin_required
2020-04-30 14:54:05 +01:00
def run_users(user=None):
update_all_user_playlists()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@blueprint.route('/playlist/run/users/cron', methods=['GET'])
@gae_cron
def run_users_cron():
update_all_user_playlists()
return jsonify({'status': 'success'}), 200
2020-03-07 21:53:52 +00:00
@blueprint.route('/playlist/image', methods=['GET'])
@login_or_basic_auth
2020-07-29 10:45:40 +01:00
@validate_args(('name', str))
2020-04-30 14:54:05 +01:00
def image(user=None):
2020-03-07 21:53:52 +00:00
2020-07-29 10:45:40 +01:00
_playlist = Playlist.collection.parent(user.key).filter('name', '==', request.args['name']).get()
2020-03-07 21:53:52 +00:00
if _playlist is None:
return jsonify({'error': "playlist not found"}), 404
2020-04-30 14:54:05 +01:00
net = database.get_authed_spotify_network(user)
2020-03-07 21:53:52 +00:00
try:
return jsonify({'images': net.get_playlist(uri_string=_playlist.uri).images, 'status': 'success'}), 200
except SpotifyNetworkException as e:
logger.exception(f'error occured during {_playlist.name} / {user.username} playlist retrieval')
return jsonify({'error': f"spotify error occured: {e.http_code}"}), 404