diff --git a/music/api/admin.py b/music/api/admin.py index 8312b10..1b0e292 100644 --- a/music/api/admin.py +++ b/music/api/admin.py @@ -3,13 +3,11 @@ from flask import Blueprint, jsonify import logging from datetime import datetime -from google.cloud import firestore from google.cloud import tasks_v2 from music.api.decorators import login_or_basic_auth, admin_required blueprint = Blueprint('admin-api', __name__) -db = firestore.Client() tasker = tasks_v2.CloudTasksClient() task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') @@ -20,7 +18,7 @@ logger = logging.getLogger(__name__) @blueprint.route('/tasks', methods=['GET']) @login_or_basic_auth @admin_required -def get_tasks(username=None): +def get_tasks(user=None): tasks = [i for i in tasker.list_tasks(task_path)] diff --git a/music/api/api.py b/music/api/api.py index d497b5e..dd02c3a 100644 --- a/music/api/api.py +++ b/music/api/api.py @@ -1,58 +1,55 @@ from flask import Blueprint, request, jsonify +from google.cloud import firestore +from werkzeug.security import generate_password_hash import os import json import logging from datetime import datetime -from google.cloud import firestore - from music.api.decorators import login_required, login_or_basic_auth, admin_required, gae_cron, cloud_task from music.cloud.tasks import execute_all_user_playlists, execute_user_playlists, create_run_user_playlist_task, \ create_play_user_playlist_task from music.tasks.run_user_playlist import run_user_playlist as run_user_playlist from music.tasks.play_user_playlist import play_user_playlist as play_user_playlist +from music.model.user import User +from music.model.playlist import Playlist + import music.db.database as database blueprint = Blueprint('api', __name__) db = firestore.Client() - logger = logging.getLogger(__name__) @blueprint.route('/playlists', methods=['GET']) @login_or_basic_auth -def get_playlists(username=None): +def get_playlists(user=None): + assert user is not None return jsonify({ - 'playlists': [i.to_dict() for i in database.get_user_playlists(username)] + 'playlists': [i.to_dict() for i in Playlist.collection.parent(user.key).fetch()] }), 200 @blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE']) @login_or_basic_auth -def playlist(username=None): - - user_playlists = database.get_user_playlists(username) - - user_ref = database.get_user(username).db_ref - playlists = user_ref.collection(u'playlists') +def playlist_route(user=None): if request.method == 'GET' or request.method == 'DELETE': playlist_name = request.args.get('name', None) if playlist_name: + playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() - queried_playlist = next((i for i in user_playlists if i.name == playlist_name), None) - - if queried_playlist is None: - return jsonify({'error': 'no playlist found'}), 404 + if playlist is None: + return jsonify({'error': f'playlist {playlist_name} not found'}), 404 if request.method == "GET": - return jsonify(queried_playlist.to_dict()), 200 + return jsonify(playlist.to_dict()), 200 elif request.method == 'DELETE': - database.delete_playlist(username=username, name=playlist_name) + Playlist.collection.parent(user.key).delete(key=playlist.key) return jsonify({"message": 'playlist deleted', "status": "success"}), 200 else: @@ -75,9 +72,9 @@ def playlist(username=None): if request_json['playlist_references'] != -1: for i in request_json['playlist_references']: - updating_playlist = database.get_playlist(username=username, name=i) + updating_playlist = Playlist.collection.parent(user.key).filter('name', '==', i).get() if updating_playlist is not None: - playlist_references.append(updating_playlist.db_ref) + playlist_references.append(db.document(updating_playlist.key)) else: return jsonify({"message": f'managed playlist {i} not found', "status": "error"}), 400 @@ -100,168 +97,143 @@ def playlist(username=None): playlist_chart_range = request_json.get('chart_range', None) playlist_chart_limit = request_json.get('chart_limit', None) - queried_playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()] + playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() if request.method == 'PUT': - if len(queried_playlist) != 0: + if playlist is not None: return jsonify({'error': 'playlist already exists'}), 400 from music.tasks.create_playlist import create_playlist as create_playlist - to_add = { - 'name': playlist_name, - 'parts': playlist_parts if playlist_parts is not None else [], - 'playlist_references': playlist_references if playlist_references is not None else [], - 'include_library_tracks': playlist_library_tracks if playlist_library_tracks is not None else False, - 'include_recommendations': playlist_recommendation if playlist_recommendation is not None else False, - 'recommendation_sample': playlist_recommendation_sample if playlist_recommendation_sample is not None else 10, - 'uri': None, - 'shuffle': playlist_shuffle if playlist_shuffle is not None else False, - 'type': playlist_type if playlist_type is not None else 'default', - 'last_updated': datetime.utcnow(), + new_db_playlist = Playlist(parent=user.key) - 'lastfm_stat_count': 0, - 'lastfm_stat_album_count': 0, - 'lastfm_stat_artist_count': 0, + new_db_playlist.name = playlist_name + new_db_playlist.parts = playlist_parts + new_db_playlist.playlist_references = playlist_references - 'lastfm_stat_percent': 0, - 'lastfm_stat_album_percent': 0, - 'lastfm_stat_artist_percent': 0, + new_db_playlist.include_library_tracks = playlist_library_tracks + new_db_playlist.include_recommendations = playlist_recommendation + new_db_playlist.recommendation_sample = playlist_recommendation_sample - 'lastfm_stat_last_refresh': datetime.utcnow(), - } + new_db_playlist.shuffle = playlist_shuffle - if user_ref.get().to_dict()['spotify_linked']: - new_playlist = create_playlist(username, playlist_name) - to_add['uri'] = str(new_playlist.uri) if new_playlist is not None else None + new_db_playlist.type = playlist_type + new_db_playlist.last_updated = datetime.utcnow() + new_db_playlist.lastfm_stat_last_refresh = datetime.utcnow() - if playlist_type == 'recents': - to_add['day_boundary'] = playlist_day_boundary if playlist_day_boundary is not None else 21 - to_add['add_this_month'] = playlist_add_this_month if playlist_add_this_month is not None else False - to_add['add_last_month'] = playlist_add_last_month if playlist_add_last_month is not None else False + new_db_playlist.day_boundary = playlist_day_boundary + new_db_playlist.add_this_month = playlist_add_this_month + new_db_playlist.add_last_month = playlist_add_last_month - if playlist_type == 'fmchart': - to_add['chart_range'] = playlist_chart_range - to_add['chart_limit'] = playlist_chart_limit if playlist_chart_limit is not None else 50 + new_db_playlist.chart_range = playlist_chart_range + new_db_playlist.chart_limit = playlist_chart_limit - playlists.document().set(to_add) - logger.info(f'added {username} / {playlist_name}') + if user.spotify_linked: + new_playlist = create_playlist(user, playlist_name) + new_db_playlist.uri = str(new_playlist.uri) + + new_db_playlist.save() + logger.info(f'added {user.username} / {playlist_name}') return jsonify({"message": 'playlist added', "status": "success"}), 201 elif request.method == 'POST': - if len(queried_playlist) == 0: + if playlist is None: return jsonify({'error': "playlist doesn't exist"}), 400 - if len(queried_playlist) > 1: - return jsonify({'error': "multiple playlists exist"}), 500 - - updating_playlist = database.get_playlist(username=username, name=playlist_name) - - dic = {} + updating_playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() if playlist_parts is not None: if playlist_parts == -1: - dic['parts'] = [] + updating_playlist.parts = [] else: - dic['parts'] = playlist_parts + updating_playlist.parts = playlist_parts if playlist_references is not None: if playlist_references == -1: - dic['playlist_references'] = [] + updating_playlist.playlist_references = [] else: - dic['playlist_references'] = playlist_references + updating_playlist.playlist_references = playlist_references if playlist_uri is not None: - dic['uri'] = playlist_uri + updating_playlist.uri = playlist_uri if playlist_shuffle is not None: - dic['shuffle'] = playlist_shuffle + updating_playlist.shuffle = playlist_shuffle if playlist_day_boundary is not None: - dic['day_boundary'] = playlist_day_boundary + updating_playlist.day_boundary = playlist_day_boundary if playlist_add_this_month is not None: - dic['add_this_month'] = playlist_add_this_month + updating_playlist.add_this_month = playlist_add_this_month if playlist_add_last_month is not None: - dic['add_last_month'] = playlist_add_last_month + updating_playlist.add_last_month = playlist_add_last_month if playlist_library_tracks is not None: - dic['include_library_tracks'] = playlist_library_tracks + updating_playlist.include_library_tracks = playlist_library_tracks if playlist_recommendation is not None: - dic['include_recommendations'] = playlist_recommendation + updating_playlist.include_recommendations = playlist_recommendation if playlist_recommendation_sample is not None: - dic['recommendation_sample'] = playlist_recommendation_sample + updating_playlist.recommendation_sample = playlist_recommendation_sample if playlist_chart_range is not None: - dic['chart_range'] = playlist_chart_range + updating_playlist.chart_range = playlist_chart_range if playlist_chart_limit is not None: - dic['chart_limit'] = playlist_chart_limit + updating_playlist.chart_limit = playlist_chart_limit if playlist_type is not None: - dic['type'] = playlist_type + # TODO check acceptable value + updating_playlist.type = playlist_type - if playlist_type == 'fmchart': - dic['chart_range'] = 'YEAR' - dic['chart_limit'] = 50 - - if len(dic) == 0: - logger.warning(f'no changes to make for {username} / {playlist_name}') - return jsonify({"message": 'no changes to make', "status": "error"}), 400 - - updating_playlist.update_database(dic) - logger.info(f'updated {username} / {playlist_name}') + updating_playlist.update() + logger.info(f'updated {user.username} / {playlist_name}') return jsonify({"message": 'playlist updated', "status": "success"}), 200 @blueprint.route('/user', methods=['GET', 'POST']) @login_or_basic_auth -def user(username=None): +def user_route(user=None): + assert user is not None if request.method == 'GET': + return jsonify(user.to_dict()), 200 - database_user = database.get_user(username) - return jsonify(database_user.to_dict()), 200 - - else: - - db_user = database.get_user(username) - - if db_user.user_type != db_user.Type.admin: - return jsonify({'status': 'error', 'message': 'unauthorized'}), 401 - + else: # POST request_json = request.get_json() if 'username' in request_json: - username = request_json['username'] + if request_json['username'].strip().lower() != user.username: + if user.type != "admin": + return jsonify({'status': 'error', 'message': 'unauthorized'}), 401 - actionable_user = database.get_user(username) + user = User.collection.filter('username', '==', request_json['username'].strip().lower()).get() if 'locked' in request_json: - logger.info(f'updating lock {username} / {request_json["locked"]}') - actionable_user.locked = request_json['locked'] + if user.type == "admin": + logger.info(f'updating lock {user.username} / {request_json["locked"]}') + user.locked = request_json['locked'] if 'spotify_linked' in request_json: - logger.info(f'deauthing {username}') + logger.info(f'deauthing {user.username}') if request_json['spotify_linked'] is False: - actionable_user.update_database({ - 'access_token': None, - 'refresh_token': None, - 'spotify_linked': False - }) + user.access_token = None + user.refresh_token = None + user.spotify_linked = False if 'lastfm_username' in request_json: - logger.info(f'updating lastfm username {username} -> {request_json["lastfm_username"]}') - actionable_user.lastfm_username = request_json['lastfm_username'] + logger.info(f'updating lastfm username {user.username} -> {request_json["lastfm_username"]}') + user.lastfm_username = request_json['lastfm_username'] - logger.info(f'updated {username}') + user.update() + + logger.info(f'updated {user.username}') return jsonify({'message': 'account updated', 'status': 'succeeded'}), 200 @@ -269,16 +241,15 @@ def user(username=None): @blueprint.route('/users', methods=['GET']) @login_or_basic_auth @admin_required -def users(username=None): +def users(user=None): return jsonify({ - 'accounts': [i.to_dict() for i in database.get_users()] + 'accounts': [i.to_dict() for i in User.collection.fetch()] }), 200 @blueprint.route('/user/password', methods=['POST']) @login_required -def change_password(username=None): - +def change_password(user=None): request_json = request.get_json() if 'new_password' in request_json and 'current_password' in request_json: @@ -289,14 +260,14 @@ def change_password(username=None): if len(request_json['new_password']) > 30: return jsonify({"error": 'password too long'}), 400 - db_user = database.get_user(username) - if db_user.check_password(request_json['current_password']): - db_user.password = request_json['new_password'] - logger.info(f'password udpated {username}') + 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}') return jsonify({"message": 'password changed', "status": "success"}), 200 else: - logger.warning(f"incorrect password {username}") + logger.warning(f"incorrect password {user.username}") return jsonify({'error': 'wrong password provided'}), 401 else: @@ -305,7 +276,7 @@ def change_password(username=None): @blueprint.route('/playlist/play', methods=['POST']) @login_or_basic_auth -def play_playlist(username=None): +def play_playlist(user=None): request_json = request.get_json() @@ -321,12 +292,12 @@ def play_playlist(username=None): request_device_name = request_json.get('device_name', None) - logger.info(f'playing {username}') + logger.info(f'playing {user.username}') if (request_parts and len(request_parts) > 0) or (request_playlists and len(request_playlists) > 0): if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': - create_play_user_playlist_task(username, + create_play_user_playlist_task(user.username, parts=request_parts, playlist_type=request_playlist_type, playlists=request_playlists, @@ -338,7 +309,7 @@ def play_playlist(username=None): add_last_month=request_add_last_month, device_name=request_device_name) else: - play_user_playlist(username, + play_user_playlist(user.username, parts=request_parts, playlist_type=request_playlist_type, playlists=request_playlists, @@ -352,7 +323,7 @@ def play_playlist(username=None): return jsonify({'message': 'execution requested', 'status': 'success'}), 200 else: - logger.error(f'no playlists/parts {username}') + logger.error(f'no playlists/parts {user.username}') return jsonify({'error': 'insufficient playlist sources'}), 400 @@ -381,16 +352,16 @@ def play_playlist_task(): @blueprint.route('/playlist/run', methods=['GET']) @login_or_basic_auth -def run_playlist(username=None): +def run_playlist(user=None): playlist_name = request.args.get('name', None) if playlist_name: if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': - create_run_user_playlist_task(username, playlist_name) + create_run_user_playlist_task(user.username, playlist_name) else: - run_user_playlist(username, playlist_name) + run_user_playlist(user.username, playlist_name) return jsonify({'message': 'execution requested', 'status': 'success'}), 200 @@ -416,13 +387,12 @@ def run_playlist_task(): @blueprint.route('/playlist/run/user', methods=['GET']) @login_or_basic_auth -def run_user(username=None): +def run_user(user=None): - db_user = database.get_user(username) - if db_user.user_type == db_user.Type.admin: - user_name = request.args.get('username', username) + if user.type == 'admin': + user_name = request.args.get('username', user.username) else: - user_name = username + user_name = user.username execute_user_playlists(user_name) @@ -442,7 +412,7 @@ def run_user_task(): @blueprint.route('/playlist/run/users', methods=['GET']) @login_or_basic_auth @admin_required -def run_users(username=None): +def run_users(user=None): execute_all_user_playlists() return jsonify({'message': 'executed all users', 'status': 'success'}), 200 @@ -458,18 +428,18 @@ def run_users_cron(): @blueprint.route('/playlist/image', methods=['GET']) @login_or_basic_auth -def image(username=None): +def image(user=None): name = request.args.get('name', None) if name is None: return jsonify({'error': "no name provided"}), 400 - _playlist = database.get_playlist(name=name, username=username) + _playlist = Playlist.collection.parent(user.key).filter('name', '==', name).get() if _playlist is None: return jsonify({'error': "playlist not found"}), 404 - net = database.get_authed_spotify_network(username=username) + net = database.get_authed_spotify_network(user) spotify_playlist = net.get_playlist(uri_string=_playlist.uri) diff --git a/music/api/decorators.py b/music/api/decorators.py index 69184d6..f1ea463 100644 --- a/music/api/decorators.py +++ b/music/api/decorators.py @@ -3,7 +3,7 @@ import logging from flask import session, request, jsonify -from music.db import database as database +from music.model.user import User logger = logging.getLogger(__name__) @@ -18,17 +18,24 @@ def is_logged_in(): def is_basic_authed(): if request.authorization: if request.authorization.get('username', None) and request.authorization.get('password', None): - if database.get_user(request.authorization.username).check_password(request.authorization.password): - return True + user = User.collection.filter('username', '==', request.authorization.username.strip().lower()).get() + if user is None: + return False, None - return False + if user.check_password(request.authorization.password): + return True, user + else: + return False, user + + return False, None def login_required(func): @functools.wraps(func) def login_required_wrapper(*args, **kwargs): if is_logged_in(): - return func(username=session['username'], *args, **kwargs) + user = User.collection.filter('username', '==', session['username'].strip().lower()).get() + return func(user=user, *args, **kwargs) else: logger.warning('user not logged in') return jsonify({'error': 'not logged in'}), 401 @@ -39,12 +46,15 @@ def login_or_basic_auth(func): @functools.wraps(func) def login_or_basic_auth_wrapper(*args, **kwargs): if is_logged_in(): - return func(username=session['username'], *args, **kwargs) - elif is_basic_authed(): - return func(username=request.authorization.username, *args, **kwargs) + user = User.collection.filter('username', '==', session['username'].strip().lower()).get() + return func(user=user, *args, **kwargs) else: - logger.warning('user not logged in') - return jsonify({'error': 'not logged in'}), 401 + check, user = is_basic_authed() + if check: + return func(user=user, *args, **kwargs) + else: + logger.warning('user not logged in') + return jsonify({'error': 'not logged in'}), 401 return login_or_basic_auth_wrapper @@ -52,10 +62,10 @@ def login_or_basic_auth(func): def admin_required(func): @functools.wraps(func) def admin_required_wrapper(*args, **kwargs): - db_user = database.get_user(kwargs.get('username')) + db_user = kwargs.get('user') if db_user is not None: - if db_user.user_type == db_user.Type.admin: + if db_user.type == 'admin': return func(*args, **kwargs) else: logger.warning(f'{db_user.username} not authorized') @@ -70,7 +80,7 @@ def admin_required(func): def spotify_link_required(func): @functools.wraps(func) def spotify_link_required_wrapper(*args, **kwargs): - db_user = database.get_user(kwargs.get('username')) + db_user = kwargs.get('user') if db_user is not None: if db_user.spotify_linked: @@ -88,7 +98,7 @@ def spotify_link_required(func): def lastfm_username_required(func): @functools.wraps(func) def lastfm_username_required_wrapper(*args, **kwargs): - db_user = database.get_user(kwargs.get('username')) + db_user = kwargs.get('user') if db_user is not None: if db_user.lastfm_username and len(db_user.lastfm_username) > 0: diff --git a/music/api/fm.py b/music/api/fm.py index 73177fa..fb1ca83 100644 --- a/music/api/fm.py +++ b/music/api/fm.py @@ -13,9 +13,9 @@ logger = logging.getLogger(__name__) @blueprint.route('/today', methods=['GET']) @login_or_basic_auth @lastfm_username_required -def daily_scrobbles(username=None): +def daily_scrobbles(user=None): - net = database.get_authed_lastfm_network(username) + net = database.get_authed_lastfm_network(user) total = net.get_scrobble_count_from_date(input_date=date.today()) diff --git a/music/api/player.py b/music/api/player.py index 81d4efb..8e39751 100644 --- a/music/api/player.py +++ b/music/api/player.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) @blueprint.route('/play', methods=['POST']) @login_or_basic_auth @spotify_link_required -def play(username=None): +def play(user=None): request_json = request.get_json() if 'uri' in request_json: @@ -27,7 +27,7 @@ def play(username=None): if uri.object_type in [Uri.ObjectType.album, Uri.ObjectType.artist, Uri.ObjectType.playlist]: context = Context(uri) - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) player = Player(net) player.play(context=context, device_name=request_json.get('device_name', None)) @@ -39,7 +39,7 @@ def play(username=None): except ValueError: return jsonify({'error': "malformed uri provided"}), 400 elif 'playlist_name' in request_json: - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) playlists = net.get_playlists() if playlists is not None: playlist_to_play = next((i for i in playlists if i.name == request_json['playlist_name']), None) @@ -60,7 +60,7 @@ def play(username=None): uris = [SpotifyTrack.wrap(uri=i) for i in uris if i.object_type == Uri.ObjectType.track] if len(uris) > 0: - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) player = Player(net) player.play(tracks=uris, device_name=request_json.get('device_name', None)) @@ -78,8 +78,8 @@ def play(username=None): @blueprint.route('/next', methods=['POST']) @login_or_basic_auth @spotify_link_required -def next_track(username=None): - net = database.get_authed_spotify_network(username) +def next_track(user=None): + net = database.get_authed_spotify_network(user) player = Player(net) player.next() @@ -89,12 +89,12 @@ def next_track(username=None): @blueprint.route('/shuffle', methods=['POST']) @login_or_basic_auth @spotify_link_required -def shuffle(username=None): +def shuffle(user=None): request_json = request.get_json() if 'state' in request_json: if isinstance(request_json['state'], bool): - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) player = Player(net) player.shuffle(state=request_json['state']) @@ -108,13 +108,13 @@ def shuffle(username=None): @blueprint.route('/volume', methods=['POST']) @login_or_basic_auth @spotify_link_required -def volume(username=None): +def volume(user=None): request_json = request.get_json() if 'volume' in request_json: if isinstance(request_json['volume'], int): if 0 <= request_json['volume'] <= 100: - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) player = Player(net) player.volume(value=request_json['volume']) diff --git a/music/api/spotfm.py b/music/api/spotfm.py index 4a9e04f..6f7b7e6 100644 --- a/music/api/spotfm.py +++ b/music/api/spotfm.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) @login_or_basic_auth @spotify_link_required @lastfm_username_required -def count(username=None): +def count(user=None): uri = request.args.get('uri', None) playlist_name = request.args.get('playlist_name', None) @@ -35,8 +35,8 @@ def count(username=None): except ValueError: return jsonify({'error': 'malformed uri provided'}), 401 - spotnet = database.get_authed_spotify_network(username) - fmnet = database.get_authed_lastfm_network(username) + spotnet = database.get_authed_spotify_network(user) + fmnet = database.get_authed_lastfm_network(user) counter = Counter(fmnet=fmnet, spotnet=spotnet) if uri: @@ -67,18 +67,18 @@ def count(username=None): @login_or_basic_auth @spotify_link_required @lastfm_username_required -def playlist_refresh(username=None): +def playlist_refresh(user=None): playlist_name = request.args.get('name', None) if playlist_name: if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': - create_refresh_playlist_task(username, playlist_name) + create_refresh_playlist_task(user.username, playlist_name) else: - refresh_lastfm_track_stats(username, playlist_name) - refresh_lastfm_album_stats(username, playlist_name) - refresh_lastfm_artist_stats(username, playlist_name) + refresh_lastfm_track_stats(user.username, playlist_name) + refresh_lastfm_album_stats(user.username, playlist_name) + refresh_lastfm_artist_stats(user.username, playlist_name) return jsonify({'message': 'execution requested', 'status': 'success'}), 200 @@ -135,7 +135,7 @@ def run_playlist_artist_task(): @blueprint.route('/playlist/refresh/users', methods=['GET']) @login_or_basic_auth @admin_required -def run_users(username=None): +def run_users(user=None): execute_all_user_playlist_stats() return jsonify({'message': 'executed all users', 'status': 'success'}), 200 @@ -149,13 +149,12 @@ def run_users_task(): @blueprint.route('/playlist/refresh/user', methods=['GET']) @login_or_basic_auth -def run_user(username=None): +def run_user(user=None): - db_user = database.get_user(username) - if db_user.type == db_user.Type.admin: - user_name = request.args.get('username', username) + if user.type == 'admin': + user_name = request.args.get('username', user.username) else: - user_name = username + user_name = user.username execute_user_playlist_stats(user_name) diff --git a/music/api/spotify.py b/music/api/spotify.py index 027aff8..faba954 100644 --- a/music/api/spotify.py +++ b/music/api/spotify.py @@ -14,10 +14,10 @@ logger = logging.getLogger(__name__) @blueprint.route('/sort', methods=['POST']) @login_or_basic_auth @spotify_link_required -def play(username=None): +def play(user=None): request_json = request.get_json() - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) engine = PlaylistEngine(net) reverse = request_json.get('reverse', False) diff --git a/music/api/tag.py b/music/api/tag.py index e690834..92c886c 100644 --- a/music/api/tag.py +++ b/music/api/tag.py @@ -2,40 +2,41 @@ from flask import Blueprint, jsonify, request import logging -import music.db.database as database from music.api.decorators import login_or_basic_auth from music.cloud.function import update_tag +from music.model.tag import Tag + blueprint = Blueprint('task', __name__) logger = logging.getLogger(__name__) @blueprint.route('/tag', methods=['GET']) @login_or_basic_auth -def tags(username=None): - logger.info(f'retrieving tags for {username}') +def tags(user=None): + logger.info(f'retrieving tags for {user.username}') return jsonify({ - 'tags': [i.to_dict() for i in database.get_user_tags(username)] + 'tags': [i.to_dict() for i in Tag.collection.parent(user.key).fetch()] }), 200 @blueprint.route('/tag/', methods=['GET', 'PUT', 'POST', "DELETE"]) @login_or_basic_auth -def tag(tag_id, username=None): +def tag_route(tag_id, user=None): if request.method == 'GET': - return get_tag(tag_id, username) + return get_tag(tag_id, user) elif request.method == 'PUT': - return put_tag(tag_id, username) + return put_tag(tag_id, user) elif request.method == 'POST': - return post_tag(tag_id, username) + return post_tag(tag_id, user) elif request.method == 'DELETE': - return delete_tag(tag_id, username) + return delete_tag(tag_id, user) -def get_tag(tag_id, username): - logger.info(f'retriving {tag_id} for {username}') +def get_tag(tag_id, user): + logger.info(f'retriving {tag_id} for {user.username}') - db_tag = database.get_tag(username=username, tag_id=tag_id) + db_tag = Tag.collection.parent(user.key).filter('tag_id', '==', tag_id).get() if db_tag is not None: return jsonify({ 'tag': db_tag.to_dict() @@ -44,10 +45,10 @@ def get_tag(tag_id, username): return jsonify({"error": 'tag not found'}), 404 -def put_tag(tag_id, username): - logger.info(f'updating {tag_id} for {username}') +def put_tag(tag_id, user): + logger.info(f'updating {tag_id} for {user.username}') - db_tag = database.get_tag(username=username, tag_id=tag_id) + db_tag = Tag.collection.parent(user.key).filter('tag_id', '==', tag_id).get() if db_tag is None: return jsonify({"error": 'tag not found'}), 404 @@ -92,34 +93,38 @@ def put_tag(tag_id, username): db_tag.artists = artists if update_required: - update_tag(username=username, tag_id=tag_id) + update_tag(username=user.username, tag_id=tag_id) + db_tag.update() return jsonify({"message": 'tag updated', "status": "success"}), 200 -def post_tag(tag_id, username): - logger.info(f'creating {tag_id} for {username}') +def post_tag(tag_id, user): + logger.info(f'creating {tag_id} for {user.username}') tag_id = tag_id.replace(' ', '_') - database.create_tag(username=username, tag_id=tag_id) + tag = Tag(parent=user.key) + tag.tag_id = tag_id + tag.name = tag_id + tag.username = user.username + tag.save() + return jsonify({"message": 'tag added', "status": "success"}), 201 -def delete_tag(tag_id, username): - logger.info(f'deleting {tag_id} for {username}') +def delete_tag(tag_id, user): + logger.info(f'deleting {tag_id} for {user.username}') - response = database.delete_tag(username=username, tag_id=tag_id) + db_tag = Tag.collection.parent(user.key).filter('tag_id', '==', tag_id).get() + Tag.collection.parent(user.key).delete(key=db_tag.key) - if response is not None: - return jsonify({"message": 'tag deleted', "status": "success"}), 201 - else: - return jsonify({"error": 'tag not deleted'}), 400 + return jsonify({"message": 'tag deleted', "status": "success"}), 201 @blueprint.route('/tag//update', methods=['GET']) @login_or_basic_auth -def tag_refresh(tag_id, username=None): - logger.info(f'updating {tag_id} tag for {username}') - update_tag(username=username, tag_id=tag_id) +def tag_refresh(tag_id, user=None): + logger.info(f'updating {tag_id} tag for {user.username}') + update_tag(username=user.username, tag_id=tag_id) return jsonify({"message": 'tag updated', "status": "success"}), 200 diff --git a/music/auth/auth.py b/music/auth/auth.py index 520afe3..2cdbce4 100644 --- a/music/auth/auth.py +++ b/music/auth/auth.py @@ -1,6 +1,7 @@ from flask import Blueprint, session, flash, request, redirect, url_for, render_template from google.cloud import firestore -from werkzeug.security import check_password_hash, generate_password_hash +from werkzeug.security import generate_password_hash +from music.model.user import User import urllib import datetime @@ -8,8 +9,6 @@ import logging from base64 import b64encode import requests -import music.db.database as database - blueprint = Blueprint('authapi', __name__) db = firestore.Client() @@ -31,8 +30,7 @@ def login(): flash('malformed request') return redirect(url_for('index')) - username = username.lower() - user = database.get_user(username) + user = User.collection.filter('username', '==', username.strip().lower()).get() if user is None: flash('user not found') @@ -45,6 +43,7 @@ def login(): return redirect(url_for('index')) user.last_login = datetime.datetime.utcnow() + user.update() logger.info(f'success {username}') session['username'] = username @@ -91,12 +90,17 @@ def register(): flash('password mismatch') return redirect('authapi.register') - if username in [i.to_dict()['username'] for i in - db.collection(u'spotify_users').where(u'username', u'==', username).stream()]: + if username in [i.username for i in + User.collection.fetch()]: flash('username already registered') return redirect('authapi.register') - database.create_user(username=username, password=password) + user = User() + user.username = username + user.password = generate_password_hash(password) + user.last_login = datetime.utcnow() + + user.save() logger.info(f'new user {username}') session['username'] = username @@ -150,15 +154,15 @@ def token(): resp = req.json() - user = database.get_user(session['username']) + user = User.collection.filter('username', '==', session['username'].strip().lower()).get() - user.update_database({ - 'access_token': resp['access_token'], - 'refresh_token': resp['refresh_token'], - 'last_refreshed': datetime.datetime.now(datetime.timezone.utc), - 'token_expiry': resp['expires_in'], - 'spotify_linked': True - }) + user.access_token = resp['access_token'] + user.refresh_token = resp['refresh_token'] + user.last_refreshed = datetime.datetime.now(datetime.timezone.utc) + user.token_expiry = resp['expires_in'] + user.spotify_linked = True + + user.update() else: flash('http error on token request') @@ -174,15 +178,15 @@ def deauth(): if 'username' in session: - user = database.get_user(session['username']) + user = User.collection.filter('username', '==', session['username'].strip().lower()).get() - user.update_database({ - 'access_token': None, - 'refresh_token': None, - 'last_refreshed': datetime.datetime.now(datetime.timezone.utc), - 'token_expiry': None, - 'spotify_linked': False - }) + user.access_token = None + user.refresh_token = None + user.last_refreshed = datetime.datetime.now(datetime.timezone.utc) + user.token_expiry = None + user.spotify_linked = False + + user.update() return redirect('/app/settings/spotify') diff --git a/music/cloud/tasks.py b/music/cloud/tasks.py index c876096..7191964 100644 --- a/music/cloud/tasks.py +++ b/music/cloud/tasks.py @@ -10,6 +10,9 @@ from music.db import database as database from music.tasks.run_user_playlist import run_user_playlist from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats +from music.model.user import User +from music.model.playlist import Playlist + tasker = tasks_v2.CloudTasksClient() task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') @@ -21,7 +24,7 @@ def execute_all_user_playlists(): seconds_delay = 0 logger.info('running') - for iter_user in database.get_users(): + for iter_user in User.collection.fetch(): if iter_user.spotify_linked and not iter_user.locked: @@ -45,14 +48,18 @@ def execute_all_user_playlists(): def execute_user_playlists(username): + user = User.collection.filter('username', '==', username.strip().lower()).get() - playlists = database.get_user_playlists(username) + if user is None: + logger.error(f'user {username} not found') + + playlists = Playlist.collection.parent(user.key).fetch() seconds_delay = 0 logger.info(f'running {username}') for iterate_playlist in playlists: - if iterate_playlist.uri: + if iterate_playlist.uri is not None: if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': create_run_user_playlist_task(username, iterate_playlist.name, seconds_delay) @@ -139,7 +146,7 @@ def execute_all_user_playlist_stats(): seconds_delay = 0 logger.info('running') - for iter_user in database.get_users(): + for iter_user in User.collection.fetch(): if iter_user.spotify_linked and iter_user.lastfm_username and \ len(iter_user.lastfm_username) > 0 and not iter_user.locked: @@ -157,15 +164,18 @@ def execute_all_user_playlist_stats(): def execute_user_playlist_stats(username): - playlists = database.get_user_playlists(username) - user = database.get_user(username) + user = User.collection.filter('username', '==', username.strip().lower()).get() + if user is None: + logger.error(f'user {username} not found') + + playlists = Playlist.collection.parent(user.key).fetch() seconds_delay = 0 logger.info(f'running {username}') if user.lastfm_username and len(user.lastfm_username) > 0: for playlist in playlists: - if playlist.uri: + if playlist.uri is not None: if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': create_refresh_playlist_task(username, playlist.name, seconds_delay) diff --git a/music/db/database.py b/music/db/database.py index 85d46b6..5209d23 100644 --- a/music/db/database.py +++ b/music/db/database.py @@ -1,15 +1,11 @@ from google.cloud import firestore import logging from datetime import timedelta, datetime, timezone -from typing import List, Optional -from werkzeug.security import generate_password_hash from spotframework.net.network import Network as SpotifyNetwork from fmframework.net.network import Network as FmNetwork from music.db.user import DatabaseUser from music.model.user import User -from music.model.playlist import Playlist, RecentsPlaylist, LastFMChartPlaylist, Sort -from music.model.tag import Tag db = firestore.Client() @@ -18,22 +14,23 @@ logger = logging.getLogger(__name__) def refresh_token_database_callback(user): if isinstance(user, DatabaseUser): - user_obj = get_user(user.user_id) + user_obj = User.collection.filter('username', '==', user.user_id.strip().lower()).get() + if user_obj is None: + logger.error(f'user {user} not found') + + user_obj.access_token = user.access_token + user_obj.refresh_token = user.refresh_token + user_obj.last_refreshed = user.last_refreshed + user_obj.token_expiry = user.token_expiry + + user_obj.update() - user_obj.update_database({ - 'access_token': user.access_token, - 'refresh_token': user.refresh_token, - 'last_refreshed': user.last_refreshed, - 'token_expiry': user.token_expiry - }) logger.debug(f'{user.user_id} database entry updated') else: logger.error('user has no attached id') -def get_authed_spotify_network(username): - - user = get_user(username) +def get_authed_spotify_network(user): if user is not None: if user.spotify_linked: spotify_keys = db.document('key/spotify').get().to_dict() @@ -41,7 +38,7 @@ def get_authed_spotify_network(username): user_obj = DatabaseUser(client_id=spotify_keys['clientid'], client_secret=spotify_keys['clientsecret'], refresh_token=user.refresh_token, - user_id=username, + user_id=user.username, access_token=user.access_token) user_obj.on_refresh.append(refresh_token_database_callback) @@ -54,403 +51,15 @@ def get_authed_spotify_network(username): else: logger.error('user spotify not linked') else: - logger.error(f'user {username} not found') + logger.error(f'user {user.username} not found') -def get_authed_lastfm_network(username): - - user = get_user(username) +def get_authed_lastfm_network(user): if user: if user.lastfm_username: fm_keys = db.document('key/fm').get().to_dict() return FmNetwork(username=user.lastfm_username, api_key=fm_keys['clientid']) else: - logger.error(f'{username} has no last.fm username') + logger.error(f'{user.username} has no last.fm username') else: - logger.error(f'user {username} not found') - - -def get_users() -> List[User]: - logger.info('retrieving users') - return [parse_user_reference(user_snapshot=i) for i in db.collection(u'spotify_users').stream()] - - -def get_user(username: str) -> Optional[User]: - logger.info(f'retrieving {username}') - - users = [i for i in db.collection(u'spotify_users').where(u'username', u'==', username).stream()] - - if len(users) == 0: - logger.error(f'user {username} not found') - return None - if len(users) > 1: - logger.critical(f"multiple {username}'s found") - return None - - return parse_user_reference(user_snapshot=users[0]) - - -def parse_user_reference(user_ref=None, user_snapshot=None) -> User: - if user_ref is None and user_snapshot is None: - raise ValueError('no user object supplied') - - if user_ref is None: - user_ref = user_snapshot.reference - - if user_snapshot is None: - user_snapshot = user_ref.get() - - user_dict = user_snapshot.to_dict() - - return User(username=user_dict.get('username'), - password=user_dict.get('password'), - db_ref=user_ref, - email=user_dict.get('email'), - user_type=User.Type[user_dict.get('type')], - last_login=user_dict.get('last_login'), - last_refreshed=user_dict.get('last_refreshed'), - locked=user_dict.get('locked'), - validated=user_dict.get('validated'), - - spotify_linked=user_dict.get('spotify_linked'), - access_token=user_dict.get('access_token'), - refresh_token=user_dict.get('refresh_token'), - token_expiry=user_dict.get('token_expiry'), - lastfm_username=user_dict.get('lastfm_username')) - - -def update_user(username: str, updates: dict) -> None: - logger.debug(f'updating {username}') - - users = [i for i in db.collection(u'spotify_users').where(u'username', u'==', username).stream()] - - if len(users) == 0: - logger.error(f'user {username} not found') - return None - if len(users) > 1: - logger.critical(f"multiple {username}'s found") - return None - - user = users[0].reference - user.update(updates) - - -def create_user(username: str, password: str): - db.collection(u'spotify_users').add({ - 'access_token': None, - 'email': None, - 'last_login': datetime.utcnow(), - 'last_refreshed': None, - 'locked': False, - 'password': generate_password_hash(password), - 'refresh_token': None, - 'spotify_linked': False, - 'type': 'user', - 'username': username, - 'validated': True - }) - - -def get_user_playlists(username: str) -> List[Playlist]: - logger.info(f'getting playlists for {username}') - - user = get_user(username) - - if user: - playlist_refs = [i for i in user.db_ref.collection(u'playlists').stream()] - - return [parse_playlist_reference(username=username, playlist_snapshot=i) for i in playlist_refs] - else: - logger.error(f'user {username} not found') - - -def get_playlist(username: str = None, name: str = None) -> Optional[Playlist]: - logger.info(f'retrieving {name} for {username}') - - user = get_user(username) - - if user: - - playlists = [i for i in user.db_ref.collection(u'playlists').where(u'name', u'==', name).stream()] - - if len(playlists) == 0: - logger.error(f'playlist {name} for {user} not found') - return None - if len(playlists) > 1: - logger.critical(f"multiple {name}'s for {user} found") - return None - - return parse_playlist_reference(username=username, playlist_snapshot=playlists[0]) - else: - logger.error(f'user {username} not found') - - -def parse_playlist_reference(username, playlist_ref=None, playlist_snapshot=None) -> Playlist: - if playlist_ref is None and playlist_snapshot is None: - raise ValueError('no playlist object supplied') - - if playlist_ref is None: - playlist_ref = playlist_snapshot.reference - - if playlist_snapshot is None: - playlist_snapshot = playlist_ref.get() - - playlist_dict = playlist_snapshot.to_dict() - - if playlist_dict.get('type') == 'default': - return Playlist(uri=playlist_dict.get('uri'), - name=playlist_dict.get('name'), - username=username, - - db_ref=playlist_ref, - - include_recommendations=playlist_dict.get('include_recommendations', False), - recommendation_sample=playlist_dict.get('recommendation_sample', 0), - include_library_tracks=playlist_dict.get('include_library_tracks', False), - - parts=playlist_dict.get('parts'), - playlist_references=playlist_dict.get('playlist_references'), - shuffle=playlist_dict.get('shuffle'), - - sort=Sort[playlist_dict.get('sort', 'release_date')], - - description_overwrite=playlist_dict.get('description_overwrite'), - description_suffix=playlist_dict.get('description_suffix'), - - last_updated=playlist_dict.get('last_updated'), - - lastfm_stat_count=playlist_dict.get('lastfm_stat_count', 0), - lastfm_stat_album_count=playlist_dict.get('lastfm_stat_album_count', 0), - lastfm_stat_artist_count=playlist_dict.get('lastfm_stat_artist_count', 0), - - lastfm_stat_percent=playlist_dict.get('lastfm_stat_percent', 0), - lastfm_stat_album_percent=playlist_dict.get('lastfm_stat_album_percent', 0), - lastfm_stat_artist_percent=playlist_dict.get('lastfm_stat_artist_percent', 0), - - lastfm_stat_last_refresh=playlist_dict.get('lastfm_stat_last_refresh')) - - elif playlist_dict.get('type') == 'recents': - return RecentsPlaylist(uri=playlist_dict.get('uri'), - name=playlist_dict.get('name'), - username=username, - - db_ref=playlist_ref, - - include_recommendations=playlist_dict.get('include_recommendations', False), - recommendation_sample=playlist_dict.get('recommendation_sample', 0), - include_library_tracks=playlist_dict.get('include_library_tracks', False), - - parts=playlist_dict.get('parts'), - playlist_references=playlist_dict.get('playlist_references'), - shuffle=playlist_dict.get('shuffle'), - - sort=Sort[playlist_dict.get('sort', 'release_date')], - - description_overwrite=playlist_dict.get('description_overwrite'), - description_suffix=playlist_dict.get('description_suffix'), - - last_updated=playlist_dict.get('last_updated'), - - lastfm_stat_count=playlist_dict.get('lastfm_stat_count', 0), - lastfm_stat_album_count=playlist_dict.get('lastfm_stat_album_count', 0), - lastfm_stat_artist_count=playlist_dict.get('lastfm_stat_artist_count', 0), - - lastfm_stat_percent=playlist_dict.get('lastfm_stat_percent', 0), - lastfm_stat_album_percent=playlist_dict.get('lastfm_stat_album_percent', 0), - lastfm_stat_artist_percent=playlist_dict.get('lastfm_stat_artist_percent', 0), - - lastfm_stat_last_refresh=playlist_dict.get('lastfm_stat_last_refresh'), - - add_last_month=playlist_dict.get('add_last_month'), - add_this_month=playlist_dict.get('add_this_month'), - day_boundary=playlist_dict.get('day_boundary')) - - elif playlist_dict.get('type') == 'fmchart': - return LastFMChartPlaylist(uri=playlist_dict.get('uri'), - name=playlist_dict.get('name'), - username=username, - - db_ref=playlist_ref, - - include_recommendations=playlist_dict.get('include_recommendations', False), - recommendation_sample=playlist_dict.get('recommendation_sample', 0), - include_library_tracks=playlist_dict.get('include_library_tracks', False), - - parts=playlist_dict.get('parts'), - playlist_references=playlist_dict.get('playlist_references'), - shuffle=playlist_dict.get('shuffle'), - - sort=Sort[playlist_dict.get('sort', 'release_date')], - - description_overwrite=playlist_dict.get('description_overwrite'), - description_suffix=playlist_dict.get('description_suffix'), - - last_updated=playlist_dict.get('last_updated'), - - lastfm_stat_count=playlist_dict.get('lastfm_stat_count', 0), - lastfm_stat_album_count=playlist_dict.get('lastfm_stat_album_count', 0), - lastfm_stat_artist_count=playlist_dict.get('lastfm_stat_artist_count', 0), - - lastfm_stat_percent=playlist_dict.get('lastfm_stat_percent', 0), - lastfm_stat_album_percent=playlist_dict.get('lastfm_stat_album_percent', 0), - lastfm_stat_artist_percent=playlist_dict.get('lastfm_stat_artist_percent', 0), - - lastfm_stat_last_refresh=playlist_dict.get('lastfm_stat_last_refresh'), - - chart_limit=playlist_dict.get('chart_limit'), - chart_range=FmNetwork.Range[playlist_dict.get('chart_range')]) - - -def update_playlist(username: str, name: str, updates: dict) -> None: - if len(updates) > 0: - logger.debug(f'updating {name} for {username}') - - user = get_user(username) - - playlists = [i for i in user.db_ref.collection(u'playlists').where(u'name', u'==', name).stream()] - - if len(playlists) == 0: - logger.error(f'playlist {name} for {username} not found') - return None - if len(playlists) > 1: - logger.critical(f"multiple {name}'s for {username} found") - return None - - playlist = playlists[0].reference - playlist.update(updates) - else: - logger.debug(f'nothing to update for {name} for {username}') - - -def delete_playlist(username: str, name: str) -> None: - logger.info(f'deleting {name} for {username}') - - playlist = get_playlist(username=username, name=name) - - if playlist: - playlist.db_ref.delete() - else: - logger.error(f'playlist {name} not found for {username}') - - -def get_user_tags(username: str) -> List[Tag]: - logger.info(f'getting tags for {username}') - - user = get_user(username) - - if user: - tag_refs = [i for i in user.db_ref.collection(u'tags').stream()] - - return [parse_tag_reference(username=username, tag_snapshot=i) for i in tag_refs] - else: - logger.error(f'user {username} not found') - - -def get_tag(username: str = None, tag_id: str = None) -> Optional[Tag]: - logger.info(f'retrieving {tag_id} for {username}') - - user = get_user(username) - - if user: - - tags = [i for i in user.db_ref.collection(u'tags').where(u'tag_id', u'==', tag_id).stream()] - - if len(tags) == 0: - logger.error(f'tag {tag_id} for {user} not found') - return None - if len(tags) > 1: - logger.critical(f"multiple {tag_id}'s for {user} found") - return None - - return parse_tag_reference(username=username, tag_snapshot=tags[0]) - else: - logger.error(f'user {username} not found') - - -def parse_tag_reference(username, tag_ref=None, tag_snapshot=None) -> Tag: - if tag_ref is None and tag_snapshot is None: - raise ValueError('no tag object supplied') - - if tag_ref is None: - tag_ref = tag_snapshot.reference - - if tag_snapshot is None: - tag_snapshot = tag_ref.get() - - tag_dict = tag_snapshot.to_dict() - - return Tag(tag_id=tag_dict['tag_id'], - name=tag_dict.get('name', 'n/a'), - username=username, - - db_ref=tag_ref, - - tracks=tag_dict.get('tracks', []), - albums=tag_dict.get('albums', []), - artists=tag_dict.get('artists', []), - - count=tag_dict.get('count', 0), - proportion=tag_dict.get('proportion', 0.0), - total_user_scrobbles=tag_dict.get('total_user_scrobbles', 0), - - last_updated=tag_dict.get('last_updated')) - - -def update_tag(username: str, tag_id: str, updates: dict) -> None: - if len(updates) > 0: - logger.debug(f'updating {tag_id} for {username}') - - user = get_user(username) - - tags = [i for i in user.db_ref.collection(u'tags').where(u'tag_id', u'==', tag_id).stream()] - - if len(tags) == 0: - logger.error(f'tag {tag_id} for {username} not found') - return None - if len(tags) > 1: - logger.critical(f"multiple {tag_id}'s for {username} found") - return None - - tag = tags[0].reference - tag.update(updates) - else: - logger.debug(f'nothing to update for {tag_id} for {username}') - - -def delete_tag(username: str, tag_id: str) -> bool: - logger.info(f'deleting {tag_id} for {username}') - - tag = get_tag(username=username, tag_id=tag_id) - - if tag: - tag.db_ref.delete() - return True - else: - logger.error(f'playlist {tag_id} not found for {username}') - return False - - -def create_tag(username: str, tag_id: str): - user = get_user(username) - - if user is None: - logger.error(f'{username} not found') - return None - - if tag_id in [i.tag_id for i in get_user_tags(username)]: - logger.error(f'{tag_id} already exists for {username}') - return None - - user.db_ref.collection(u'tags').add({ - 'tag_id': tag_id, - 'name': tag_id, - - 'tracks': [], - 'albums': [], - 'artists': [], - - 'count': 0, - 'proportion': 0.0, - 'total_user_scrobbles': 0, - 'last_updated': None - }) + logger.error(f'user {user.username} not found') diff --git a/music/db/part_generator.py b/music/db/part_generator.py index a3437af..eaa7a85 100644 --- a/music/db/part_generator.py +++ b/music/db/part_generator.py @@ -1,9 +1,7 @@ -from google.cloud import firestore -import music.db.database as database from music.model.user import User +from music.model.playlist import Playlist import logging -db = firestore.Client() logger = logging.getLogger(__name__) @@ -16,7 +14,7 @@ class PartGenerator: if user: self.user = user elif username: - pulled_user = database.get_user(username) + pulled_user = User.collection.filter('username', '==', username.strip().lower()).get() if pulled_user: self.user = pulled_user else: @@ -38,13 +36,14 @@ class PartGenerator: def process_reference_by_name(self, name): - playlist = database.get_playlist(username=self.user.username, name=name) + playlist = Playlist.collection.parent(self.user.key).filter('name', '==', name).get() if playlist is not None: - if playlist.db_ref.id not in self.queried_playlists: + if playlist.id not in self.queried_playlists: self.parts += playlist.parts + self.queried_playlists.append(playlist.id) for i in playlist.playlist_references: if i.id not in self.queried_playlists: @@ -61,6 +60,7 @@ class PartGenerator: if ref.id not in self.queried_playlists: playlist_reference_object = ref.get().to_dict() self.parts += playlist_reference_object['parts'] + self.queried_playlists.append(ref.id) for i in playlist_reference_object['playlist_references']: self.process_reference_by_reference(i) diff --git a/music/model/playlist.py b/music/model/playlist.py index f9b9e03..7e286d7 100644 --- a/music/model/playlist.py +++ b/music/model/playlist.py @@ -1,11 +1,7 @@ -from typing import List from enum import Enum -from datetime import datetime -from google.cloud.firestore import DocumentReference -from fmframework.net.network import Network - -import music.db.database as database +from fireo.models import Model +from fireo.fields import TextField, BooleanField, DateTime, NumberField, ListField class Sort(Enum): @@ -14,466 +10,52 @@ class Sort(Enum): release_date = 3 -class Playlist: - def __init__(self, - uri: str, - name: str, - username: str, +class Playlist(Model): + class Meta: + collection_name = 'playlists' - db_ref: DocumentReference, + uri = TextField() + name = TextField(required=True) + type = TextField(required=True) - include_recommendations: bool, - recommendation_sample: int, - include_library_tracks: bool, + include_recommendations = BooleanField(default=False) + recommendation_sample = NumberField(default=10) + include_library_tracks = BooleanField(default=False) - parts: List[str], - playlist_references: List[DocumentReference], - shuffle: bool, + parts = ListField(default=[]) + playlist_references = ListField(default=[]) + shuffle = BooleanField(default=False) - sort: Sort = None, + sort = TextField(default='release_date') + description_overwrite = TextField() + description_suffix = TextField() - description_overwrite: str = None, - description_suffix: str = None, + last_updated = DateTime() - last_updated: datetime = None, + lastfm_stat_count = NumberField(default=0) + lastfm_stat_album_count = NumberField(default=0) + lastfm_stat_artist_count = NumberField(default=0) - lastfm_stat_count: int = None, - lastfm_stat_album_count: int = None, - lastfm_stat_artist_count: int = None, + lastfm_stat_percent = NumberField(default=0) + lastfm_stat_album_percent = NumberField(default=0) + lastfm_stat_artist_percent = NumberField(default=0) - lastfm_stat_percent: int = None, - lastfm_stat_album_percent: int = None, - lastfm_stat_artist_percent: int = None, + lastfm_stat_last_refresh = DateTime() - lastfm_stat_last_refresh: datetime = None): - self._uri = uri - self.name = name - self.username = username + add_last_month = BooleanField(default=False) + add_this_month = BooleanField(default=False) + day_boundary = NumberField(default=21) - self.db_ref = db_ref - - self._include_recommendations = include_recommendations - self._recommendation_sample = recommendation_sample - self._include_library_tracks = include_library_tracks - - self._parts = parts - self._playlist_references = playlist_references - self._shuffle = shuffle - - self._sort = sort - self._description_overwrite = description_overwrite - self._description_suffix = description_suffix - - self._last_updated = last_updated - - self._lastfm_stat_count = lastfm_stat_count - self._lastfm_stat_album_count = lastfm_stat_album_count - self._lastfm_stat_artist_count = lastfm_stat_artist_count - - self._lastfm_stat_percent = lastfm_stat_percent - self._lastfm_stat_album_percent = lastfm_stat_album_percent - self._lastfm_stat_artist_percent = lastfm_stat_artist_percent - - self._lastfm_stat_last_refresh = lastfm_stat_last_refresh + chart_range = TextField(default='1month') + chart_limit = NumberField(default=50) def to_dict(self): - return { - 'uri': self.uri, - 'name': self.name, - 'type': 'default', + to_return = super().to_dict() - 'include_recommendations': self.include_recommendations, - 'recommendation_sample': self.recommendation_sample, - 'include_library_tracks': self.include_library_tracks, + to_return["playlist_references"] = [i.get().to_dict().get('name') for i in to_return['playlist_references']] - 'parts': self.parts, - 'playlist_references': [i.get().to_dict().get('name') for i in self.playlist_references], - 'shuffle': self.shuffle, + # remove unnecessary and sensitive fields + to_return.pop('id', None) + to_return.pop('key', None) - 'sort': self.sort.name, - 'description_overwrite': self.description_overwrite, - 'description_suffix': self.description_suffix, - - 'last_updated': self.last_updated, - - 'lastfm_stat_count': self.lastfm_stat_count, - 'lastfm_stat_album_count': self.lastfm_stat_album_count, - 'lastfm_stat_artist_count': self.lastfm_stat_artist_count, - - 'lastfm_stat_percent': self.lastfm_stat_percent, - 'lastfm_stat_album_percent': self.lastfm_stat_album_percent, - 'lastfm_stat_artist_percent': self.lastfm_stat_artist_percent, - - 'lastfm_stat_last_refresh': self.lastfm_stat_last_refresh - } - - def update_database(self, updates): - database.update_playlist(username=self.username, name=self.name, updates=updates) - - @property - def uri(self): - return self._uri - - @uri.setter - def uri(self, value): - database.update_playlist(self.username, self.name, {'uri': value}) - self._uri = value - - @property - def include_recommendations(self): - return self._include_recommendations - - @include_recommendations.setter - def include_recommendations(self, value): - database.update_playlist(self.username, self.name, {'include_recommendations': value}) - self._include_recommendations = value - - @property - def recommendation_sample(self): - return self._recommendation_sample - - @recommendation_sample.setter - def recommendation_sample(self, value): - database.update_playlist(self.username, self.name, {'recommendation_sample': value}) - self._recommendation_sample = value - - @property - def include_library_tracks(self): - return self._include_library_tracks - - @include_library_tracks.setter - def include_library_tracks(self, value): - database.update_playlist(self.username, self.name, {'include_library_tracks': value}) - self._include_library_tracks = value - - @property - def parts(self): - return self._parts - - @parts.setter - def parts(self, value): - database.update_playlist(self.username, self.name, {'parts': value}) - self._parts = value - - @property - def playlist_references(self): - return self._playlist_references - - @playlist_references.setter - def playlist_references(self, value): - database.update_playlist(self.username, self.name, {'playlist_references': value}) - self._playlist_references = value - - @property - def shuffle(self): - return self._shuffle - - @shuffle.setter - def shuffle(self, value): - database.update_playlist(self.username, self.name, {'shuffle': value}) - self._shuffle = value - - @property - def sort(self): - return self._sort - - @sort.setter - def sort(self, value): - database.update_playlist(self.username, self.name, {'sort': value.name}) - self._sort = value - - @property - def description_overwrite(self): - return self._description_overwrite - - @description_overwrite.setter - def description_overwrite(self, value): - database.update_playlist(self.username, self.name, {'description_overwrite': value}) - self._description_overwrite = value - - @property - def description_suffix(self): - return self._description_suffix - - @description_suffix.setter - def description_suffix(self, value): - database.update_playlist(self.username, self.name, {'description_suffix': value}) - self._description_suffix = value - - @property - def last_updated(self): - return self._last_updated - - @last_updated.setter - def last_updated(self, value): - database.update_playlist(self.username, self.name, {'last_updated': value}) - self._last_updated = value - - @property - def lastfm_stat_count(self): - return self._lastfm_stat_count - - @lastfm_stat_count.setter - def lastfm_stat_count(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_count': value}) - self._lastfm_stat_count = value - - @property - def lastfm_stat_album_count(self): - return self._lastfm_stat_album_count - - @lastfm_stat_album_count.setter - def lastfm_stat_album_count(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_album_count': value}) - self._lastfm_stat_album_count = value - - @property - def lastfm_stat_artist_count(self): - return self._lastfm_stat_artist_count - - @lastfm_stat_artist_count.setter - def lastfm_stat_artist_count(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_artist_count': value}) - self._lastfm_stat_artist_count = value - - @property - def lastfm_stat_percent(self): - return self._lastfm_stat_percent - - @lastfm_stat_percent.setter - def lastfm_stat_percent(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_percent': value}) - self._lastfm_stat_percent = value - - @property - def lastfm_stat_album_percent(self): - return self._lastfm_stat_album_percent - - @lastfm_stat_album_percent.setter - def lastfm_stat_album_percent(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_album_percent': value}) - self._lastfm_stat_album_percent = value - - @property - def lastfm_stat_artist_percent(self): - return self._lastfm_stat_artist_percent - - @lastfm_stat_artist_percent.setter - def lastfm_stat_artist_percent(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_artist_percent': value}) - self._lastfm_stat_artist_percent = value - - @property - def lastfm_stat_last_refresh(self): - return self._lastfm_stat_last_refresh - - @lastfm_stat_last_refresh.setter - def lastfm_stat_last_refresh(self, value): - database.update_playlist(self.username, self.name, {'lastfm_stat_last_refresh': value}) - self._lastfm_stat_last_refresh = value - - -class RecentsPlaylist(Playlist): - def __init__(self, - uri: str, - name: str, - username: str, - - db_ref: DocumentReference, - - include_recommendations: bool, - recommendation_sample: int, - include_library_tracks: bool, - - parts: List[str], - playlist_references: List[DocumentReference], - shuffle: bool, - - sort: Sort = None, - - description_overwrite: str = None, - description_suffix: str = None, - - last_updated: datetime = None, - - lastfm_stat_count: int = None, - lastfm_stat_album_count: int = None, - lastfm_stat_artist_count: int = None, - - lastfm_stat_percent: int = None, - lastfm_stat_album_percent: int = None, - lastfm_stat_artist_percent: int = None, - - lastfm_stat_last_refresh: datetime = None, - - add_last_month: bool = False, - add_this_month: bool = False, - day_boundary: int = 7): - super().__init__(uri=uri, - name=name, - username=username, - - db_ref=db_ref, - - include_recommendations=include_recommendations, - recommendation_sample=recommendation_sample, - include_library_tracks=include_library_tracks, - - parts=parts, - playlist_references=playlist_references, - shuffle=shuffle, - - sort=sort, - - description_overwrite=description_overwrite, - description_suffix=description_suffix, - - last_updated=last_updated, - - lastfm_stat_count=lastfm_stat_count, - lastfm_stat_album_count=lastfm_stat_album_count, - lastfm_stat_artist_count=lastfm_stat_artist_count, - - lastfm_stat_percent=lastfm_stat_percent, - lastfm_stat_album_percent=lastfm_stat_album_percent, - lastfm_stat_artist_percent=lastfm_stat_artist_percent, - - lastfm_stat_last_refresh=lastfm_stat_last_refresh) - self._add_last_month = add_last_month - self._add_this_month = add_this_month - self._day_boundary = day_boundary - - def to_dict(self): - response = super().to_dict() - response.update({ - 'add_last_month': self.add_last_month, - 'add_this_month': self.add_this_month, - 'day_boundary': self.day_boundary, - 'type': 'recents' - }) - return response - - @property - def add_last_month(self): - return self._add_last_month - - @add_last_month.setter - def add_last_month(self, value): - database.update_playlist(self.username, self.name, {'add_last_month': value}) - self._add_last_month = value - - @property - def add_this_month(self): - return self._add_this_month - - @add_this_month.setter - def add_this_month(self, value): - database.update_playlist(self.username, self.name, {'add_this_month': value}) - self._add_this_month = value - - @property - def day_boundary(self): - return self._day_boundary - - @day_boundary.setter - def day_boundary(self, value): - database.update_playlist(self.username, self.name, {'day_boundary': value}) - self._day_boundary = value - - -class LastFMChartPlaylist(Playlist): - def __init__(self, - uri: str, - name: str, - username: str, - - chart_range: Network.Range, - - db_ref: DocumentReference, - - - include_recommendations: bool, - recommendation_sample: int, - include_library_tracks: bool, - - parts: List[str], - playlist_references: List[DocumentReference], - shuffle: bool, - - chart_limit: int = 50, - - sort: Sort = None, - - description_overwrite: str = None, - description_suffix: str = None, - - last_updated: datetime = None, - - lastfm_stat_count: int = None, - lastfm_stat_album_count: int = None, - lastfm_stat_artist_count: int = None, - - lastfm_stat_percent: int = None, - lastfm_stat_album_percent: int = None, - lastfm_stat_artist_percent: int = None, - - lastfm_stat_last_refresh: datetime = None): - super().__init__(uri=uri, - name=name, - username=username, - - db_ref=db_ref, - - include_recommendations=include_recommendations, - recommendation_sample=recommendation_sample, - include_library_tracks=include_library_tracks, - - parts=parts, - playlist_references=playlist_references, - shuffle=shuffle, - - sort=sort, - - description_overwrite=description_overwrite, - description_suffix=description_suffix, - - last_updated=last_updated, - - lastfm_stat_count=lastfm_stat_count, - lastfm_stat_album_count=lastfm_stat_album_count, - lastfm_stat_artist_count=lastfm_stat_artist_count, - - lastfm_stat_percent=lastfm_stat_percent, - lastfm_stat_album_percent=lastfm_stat_album_percent, - lastfm_stat_artist_percent=lastfm_stat_artist_percent, - - lastfm_stat_last_refresh=lastfm_stat_last_refresh) - self._chart_range = chart_range - self._chart_limit = chart_limit - - def to_dict(self): - response = super().to_dict() - response.update({ - 'chart_limit': self.chart_limit, - 'chart_range': self.chart_range.name, - 'type': 'fmchart' - }) - return response - - @property - def chart_range(self): - return self._chart_range - - @chart_range.setter - def chart_range(self, value): - database.update_playlist(self.username, self.name, {'chart_range': value.name}) - self._chart_range = value - - @property - def chart_limit(self): - return self._chart_limit - - @chart_limit.setter - def chart_limit(self, value): - database.update_playlist(self.username, self.name, {'chart_limit': value}) - self._chart_limit = value + return to_return diff --git a/music/model/tag.py b/music/model/tag.py index 88f55f4..bb9faa7 100644 --- a/music/model/tag.py +++ b/music/model/tag.py @@ -1,129 +1,30 @@ -from datetime import datetime -import music.db.database as db +from fireo.models import Model +from fireo.fields import TextField, DateTime, NumberField, ListField -class Tag: +class Tag(Model): + class Meta: + collection_name = 'tags' - def __init__(self, - tag_id: str, - name: str, - username: str, + tag_id = TextField(required=True) + name = TextField(required=True) + username = TextField(required=True) - db_ref, + tracks = ListField(default=[]) + albums = ListField(default=[]) + artists = ListField(default=[]) - tracks, - albums, - artists, + count = NumberField(default=0) + proportion = NumberField(default=0) + total_user_scrobbles = NumberField(default=0) - count: int, - proportion: float, - total_user_scrobbles: int, - - last_updated: datetime): - self.tag_id = tag_id - self._name = name - self.username = username - - self.db_ref = db_ref - - self._tracks = tracks - self._albums = albums - self._artists = artists - - self._count = count - self._proportion = proportion - self._total_user_scrobbles = total_user_scrobbles - - self._last_updated = last_updated + last_updated = DateTime() def to_dict(self): - return { - 'tag_id': self.tag_id, - 'name': self.name, - 'username': self.username, + to_return = super().to_dict() - 'tracks': self.tracks, - 'albums': self.albums, - 'artists': self.artists, + # remove unnecessary and sensitive fields + to_return.pop('id', None) + to_return.pop('key', None) - 'count': self.count, - 'proportion': self.proportion, - 'total_user_scrobbles': self.total_user_scrobbles, - - 'last_updated': self.last_updated - } - - def update_database(self, updates): - db.update_tag(username=self.username, tag_id=self.tag_id, updates=updates) - - @property - def name(self): - return self._name - - @name.setter - def name(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'name': value}) - self._name = value - - @property - def tracks(self): - return self._tracks - - @tracks.setter - def tracks(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'tracks': value}) - self._tracks = value - - @property - def albums(self): - return self._albums - - @albums.setter - def albums(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'albums': value}) - self._albums = value - - @property - def artists(self): - return self._artists - - @artists.setter - def artists(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'artists': value}) - self._artists = value - - @property - def count(self): - return self._count - - @count.setter - def count(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'count': value}) - self._count = value - - @property - def proportion(self): - return self._proportion - - @proportion.setter - def proportion(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'proportion': value}) - self._proportion = value - - @property - def total_user_scrobbles(self): - return self._total_user_scrobbles - - @total_user_scrobbles.setter - def total_user_scrobbles(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'total_user_scrobbles': value}) - self._total_user_scrobbles = value - - @property - def last_updated(self): - return self._last_updated - - @last_updated.setter - def last_updated(self, value): - db.update_tag(username=self.username, tag_id=self.tag_id, updates={'last_updated': value}) - self._last_updated = value + return to_return diff --git a/music/model/user.py b/music/model/user.py index 1422a12..0a6015d 100644 --- a/music/model/user.py +++ b/music/model/user.py @@ -1,172 +1,42 @@ -from datetime import datetime -from enum import Enum +from fireo.models import Model +from fireo.fields import TextField, BooleanField, DateTime, NumberField -from werkzeug.security import generate_password_hash, check_password_hash - -import music.db.database as database +from werkzeug.security import check_password_hash -class User: - class Type(Enum): - user = 1 - admin = 2 +class User(Model): + class Meta: + collection_name = 'spotify_users' - def __init__(self, - username: str, - password: str, - db_ref, - email: str, - user_type: Type, - last_login: datetime, - last_refreshed: datetime, - locked: bool, - validated: bool, + username = TextField(required=True) + password = TextField(required=True) + email = TextField() + type = TextField(default="user") - spotify_linked: bool, - access_token: str, - refresh_token: str, - token_expiry: int, + last_login = DateTime() + last_refreshed = DateTime() + locked = BooleanField(default=False, required=True) + validated = BooleanField(default=True, required=True) - lastfm_username: str = None): - self.username = username - self._password = password - self.db_ref = db_ref - self._email = email - self._type = user_type + spotify_linked = BooleanField(default=False, required=True) + access_token = TextField() + refresh_token = TextField() + token_expiry = NumberField() - self._last_login = last_login - self._last_refreshed = last_refreshed - self._locked = locked - self._validated = validated - - self._spotify_linked = spotify_linked - self._access_token = access_token - self._refresh_token = refresh_token - self._token_expiry = token_expiry - - self._lastfm_username = lastfm_username + lastfm_username = TextField() def check_password(self, password): return check_password_hash(self.password, password) def to_dict(self): - return { - 'username': self.username, - 'email': self.email, - 'type': self.user_type.name, - 'last_login': self.last_login, - 'spotify_linked': self.spotify_linked, - 'lastfm_username': self.lastfm_username - } + to_return = super().to_dict() - def update_database(self, updates): - database.update_user(username=self.username, updates=updates) + # remove unnecessary and sensitive fields + to_return.pop('password', None) + to_return.pop('access_token', None) + to_return.pop('refresh_token', None) + to_return.pop('token_expiry', None) + to_return.pop('id', None) + to_return.pop('key', None) - @property - def password(self): - return self._password - - @password.setter - def password(self, value): - pw_hash = generate_password_hash(value) - database.update_user(self.username, {'password': pw_hash}) - self._password = pw_hash - - @property - def email(self): - return self._email - - @email.setter - def email(self, value): - database.update_user(self.username, {'email': value}) - self._email = value - - @property - def user_type(self): - return self._type - - @user_type.setter - def user_type(self, value): - database.update_user(self.username, {'type': value}) - self._type = value - - @property - def last_login(self): - return self._last_login - - @last_login.setter - def last_login(self, value): - database.update_user(self.username, {'last_login': value}) - self._last_login = value - - @property - def last_refreshed(self): - return self._last_refreshed - - @last_refreshed.setter - def last_refreshed(self, value): - database.update_user(self.username, {'last_refreshed': value}) - self._last_refreshed = value - - @property - def locked(self): - return self._locked - - @locked.setter - def locked(self, value): - database.update_user(self.username, {'locked': value}) - self._locked = value - - @property - def validated(self): - return self._validated - - @validated.setter - def validated(self, value): - database.update_user(self.username, {'validated': value}) - self._validated = value - - @property - def spotify_linked(self): - return self._spotify_linked - - @spotify_linked.setter - def spotify_linked(self, value): - database.update_user(self.username, {'spotify_linked': value}) - self._spotify_linked = value - - @property - def access_token(self): - return self._access_token - - @access_token.setter - def access_token(self, value): - database.update_user(self.username, {'access_token': value}) - self._access_token = value - - @property - def refresh_token(self): - return self._refresh_token - - @refresh_token.setter - def refresh_token(self, value): - database.update_user(self.username, {'refresh_token': value}) - self._refresh_token = value - - @property - def token_expiry(self): - return self._token_expiry - - @token_expiry.setter - def token_expiry(self, value): - database.update_user(self.username, {'refresh_token': value}) - self._token_expiry = value - - @property - def lastfm_username(self): - return self._lastfm_username - - @lastfm_username.setter - def lastfm_username(self, value): - database.update_user(self.username, {'lastfm_username': value}) - self._lastfm_username = value + return to_return diff --git a/music/tasks/create_playlist.py b/music/tasks/create_playlist.py index 26e9004..4a8ccbd 100644 --- a/music/tasks/create_playlist.py +++ b/music/tasks/create_playlist.py @@ -9,21 +9,19 @@ db = firestore.Client() logger = logging.getLogger(__name__) -def create_playlist(username, name): - logger.info(f'creating spotify playlist for {username} / {name}') +def create_playlist(user, name): + logger.info(f'creating spotify playlist for {user.username} / {name}') - user = database.get_user(username) if user is not None: - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) playlist = net.create_playlist(net.user.username, name) if playlist is not None: return playlist else: - logger.error(f'no response received {username} / {name}') + logger.error(f'no response received {user.username} / {name}') return else: - logger.error(f'{username} not found') - return + logger.error(f'{user.username} not provided') diff --git a/music/tasks/play_user_playlist.py b/music/tasks/play_user_playlist.py index da99928..b8241c8 100644 --- a/music/tasks/play_user_playlist.py +++ b/music/tasks/play_user_playlist.py @@ -1,5 +1,3 @@ -from google.cloud import firestore - import datetime import logging @@ -11,8 +9,7 @@ from spotframework.engine.processor.deduplicate import DeduplicateByID from spotframework.player.player import Player import music.db.database as database from music.db.part_generator import PartGenerator - -db = firestore.Client() +from music.model.user import User logger = logging.getLogger(__name__) @@ -29,7 +26,11 @@ def play_user_playlist(username, add_last_month=False, device_name=None): - user = database.get_user(username) + user = User.collection.filter('username', '==', username.strip().lower()).get() + + if user is None: + logger.error(f'user {username} not found') + return logger.info(f'playing for {username}') @@ -51,7 +52,7 @@ def play_user_playlist(username, logger.critical(f'no playlists to use for creation ({username})') return None - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) device = None if device_name: diff --git a/music/tasks/refresh_lastfm_stats.py b/music/tasks/refresh_lastfm_stats.py index 39e2615..c3f5bfd 100644 --- a/music/tasks/refresh_lastfm_stats.py +++ b/music/tasks/refresh_lastfm_stats.py @@ -4,6 +4,8 @@ import logging from datetime import datetime import music.db.database as database +from music.model.user import User +from music.model.playlist import Playlist from spotfm.maths.counter import Counter from spotframework.model.uri import Uri @@ -17,11 +19,15 @@ def refresh_lastfm_track_stats(username, playlist_name): logger.info(f'refreshing {playlist_name} stats for {username}') - fmnet = database.get_authed_lastfm_network(username=username) - spotnet = database.get_authed_spotify_network(username=username) + user = User.collection.filter('username', '==', username.strip().lower()).get() + if user is None: + logger.error(f'user {username} not found') + + fmnet = database.get_authed_lastfm_network(user) + spotnet = database.get_authed_spotify_network(user) counter = Counter(fmnet=fmnet, spotnet=spotnet) - playlist = database.get_playlist(username=username, name=playlist_name) + playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() if playlist is None: logger.critical(f'playlist {playlist_name} for {username} not found') @@ -40,23 +46,26 @@ def refresh_lastfm_track_stats(username, playlist_name): else: percent = 0 - playlist.update_database({ - 'lastfm_stat_count': track_count, - 'lastfm_stat_percent': percent, + playlist.lastfm_stat_count = track_count + playlist.lastfm_stat_percent = percent + playlist.lastfm_stat_last_refresh = datetime.utcnow() - 'lastfm_stat_last_refresh': datetime.utcnow() - }) + playlist.update() def refresh_lastfm_album_stats(username, playlist_name): logger.info(f'refreshing {playlist_name} stats for {username}') - fmnet = database.get_authed_lastfm_network(username=username) - spotnet = database.get_authed_spotify_network(username=username) + user = User.collection.filter('username', '==', username.strip().lower()).get() + if user is None: + logger.error(f'user {username} not found') + + fmnet = database.get_authed_lastfm_network(user) + spotnet = database.get_authed_spotify_network(user) counter = Counter(fmnet=fmnet, spotnet=spotnet) - playlist = database.get_playlist(username=username, name=playlist_name) + playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() if playlist is None: logger.critical(f'playlist {playlist_name} for {username} not found') @@ -75,23 +84,26 @@ def refresh_lastfm_album_stats(username, playlist_name): else: album_percent = 0 - playlist.update_database({ - 'lastfm_stat_album_count': album_count, - 'lastfm_stat_album_percent': album_percent, + playlist.lastfm_stat_album_count = album_count + playlist.lastfm_stat_album_percent = album_percent + playlist.lastfm_stat_last_refresh = datetime.utcnow() - 'lastfm_stat_last_refresh': datetime.utcnow() - }) + playlist.update() def refresh_lastfm_artist_stats(username, playlist_name): logger.info(f'refreshing {playlist_name} stats for {username}') - fmnet = database.get_authed_lastfm_network(username=username) - spotnet = database.get_authed_spotify_network(username=username) + user = User.collection.filter('username', '==', username.strip().lower()).get() + if user is None: + logger.error(f'user {username} not found') + + fmnet = database.get_authed_lastfm_network(user) + spotnet = database.get_authed_spotify_network(user) counter = Counter(fmnet=fmnet, spotnet=spotnet) - playlist = database.get_playlist(username=username, name=playlist_name) + playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() if playlist is None: logger.critical(f'playlist {playlist_name} for {username} not found') @@ -110,9 +122,8 @@ def refresh_lastfm_artist_stats(username, playlist_name): else: artist_percent = 0 - playlist.update_database({ - 'lastfm_stat_artist_count': artist_count, - 'lastfm_stat_artist_percent': artist_percent, + playlist.lastfm_stat_artist_count = artist_count + playlist.lastfm_stat_artist_percent = artist_percent + playlist.lastfm_stat_last_refresh = datetime.utcnow() - 'lastfm_stat_last_refresh': datetime.utcnow() - }) + playlist.update() diff --git a/music/tasks/run_user_playlist.py b/music/tasks/run_user_playlist.py index 2b93674..bda67ff 100644 --- a/music/tasks/run_user_playlist.py +++ b/music/tasks/run_user_playlist.py @@ -14,7 +14,8 @@ from spotfm.engine.chart_source import ChartSource import music.db.database as database from music.db.part_generator import PartGenerator -from music.model.playlist import RecentsPlaylist, LastFMChartPlaylist +from music.model.user import User +from music.model.playlist import Playlist db = firestore.Client() @@ -23,7 +24,9 @@ logger = logging.getLogger(__name__) def run_user_playlist(username, playlist_name): """Generate and upadate a user's playlist""" - user = database.get_user(username) + user = User.collection.filter('username', '==', username.strip().lower()).get() + if user is None: + logger.error(f'user {username} not found') logger.info(f'running {username} / {playlist_name}') @@ -32,7 +35,7 @@ def run_user_playlist(username, playlist_name): logger.critical(f'{username} not found') return - playlist = database.get_playlist(username=username, name=playlist_name) + playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get() if playlist is None: logger.critical(f'playlist not found ({username}/{playlist_name})') @@ -44,7 +47,7 @@ def run_user_playlist(username, playlist_name): # END CHECKS - net = database.get_authed_spotify_network(username) + net = database.get_authed_spotify_network(user) engine = PlaylistEngine(net) part_generator = PartGenerator(user=user) @@ -63,11 +66,11 @@ def run_user_playlist(username, playlist_name): params.append(LibraryTrackSource.Params()) # END OPTIONS - if isinstance(playlist, LastFMChartPlaylist): + if playlist.type == 'fmchart': if user.lastfm_username is None: logger.error(f'{username} has no associated last.fm username, chart source skipped') else: - engine.sources.append(ChartSource(spotnet=net, fmnet=database.get_authed_lastfm_network(user.username))) + engine.sources.append(ChartSource(spotnet=net, fmnet=database.get_authed_lastfm_network(user))) params.append(ChartSource.Params(chart_range=playlist.chart_range, limit=playlist.chart_limit)) else: @@ -78,7 +81,7 @@ def run_user_playlist(username, playlist_name): processors.append(SortReleaseDate(reverse=True)) # GENERATE TRACKS - if isinstance(playlist, RecentsPlaylist): + if playlist.type == 'recents': boundary_date = datetime.datetime.now(datetime.timezone.utc) - \ datetime.timedelta(days=int(playlist.day_boundary)) tracks = engine.get_recent_playlist(params=params, @@ -101,3 +104,4 @@ def run_user_playlist(username, playlist_name): overwrite=overwrite, suffix=suffix) playlist.last_updated = datetime.datetime.utcnow() + playlist.update() diff --git a/music/tasks/update_tag.py b/music/tasks/update_tag.py index d1e5fe1..b030cd5 100644 --- a/music/tasks/update_tag.py +++ b/music/tasks/update_tag.py @@ -2,6 +2,8 @@ import logging from datetime import datetime import music.db.database as database +from music.model.user import User +from music.model.tag import Tag logger = logging.getLogger(__name__) @@ -9,19 +11,20 @@ logger = logging.getLogger(__name__) def update_tag(username, tag_id): logger.info(f'updating {username} / {tag_id}') - tag = database.get_tag(username=username, tag_id=tag_id) + user = User.collection.filter('username', '==', username.strip().lower()).get() + if user is None: + logger.error(f'user {username} not found') + tag = Tag.collection.parent(user.key).filter('tag_id', '==', tag_id).get() if tag is None: logger.error(f'{tag_id} for {username} not found') return - user = database.get_user(username) - if user.lastfm_username is None or len(user.lastfm_username) == 0: logger.error(f'{username} has no last.fm username') return - net = database.get_authed_lastfm_network(username=username) + net = database.get_authed_lastfm_network(user) tag_count = 0 user_scrobbles = net.get_user_scrobble_count() @@ -60,13 +63,13 @@ def update_tag(username, tag_id): tracks.append(track) - tag.update_database({ - 'tracks': tracks, - 'albums': albums, - 'artists': artists, + tag.tracks = tracks + tag.albums = albums + tag.artists = artists - 'total_user_scrobbles': user_scrobbles, - 'count': tag_count, - 'proportion': (tag_count / user_scrobbles) * 100, - 'last_updated': datetime.utcnow() - }) + tag.total_user_scrobbles = user_scrobbles + tag.count = tag_count + tag.proportion = (tag_count / user_scrobbles) * 100 + tag.last_updated = datetime.utcnow() + + tag.update() diff --git a/rename_playlist.py b/rename_playlist.py index 991be38..5d45daf 100644 --- a/rename_playlist.py +++ b/rename_playlist.py @@ -1,13 +1,14 @@ -import music.db.database as database +from music.model.user import User +from music.model.playlist import Playlist -playlists = database.get_user_playlists('andy') +user = User.collection.filter('username', '==', 'andy').get() name = input('enter playlist name: ') - -playlist = next((i for i in playlists if i.name == name), None) +playlist = Playlist.collection.parent(user.key).filter('name', '==', name).get() if playlist is not None: new_name = input('enter new name: ') - playlist.update_database({'name': new_name}) + playlist.name = new_name + playlist.update() else: print('playlist not found') diff --git a/requirements.txt b/requirements.txt index 9444dd6..b8889c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,37 +1,39 @@ -astroid==2.3.3 -cachetools==4.0.0 -certifi==2019.11.28 +astroid==2.4.0 +cachetools==4.1.0 +certifi==2020.4.5.1 chardet==3.0.4 -click==7.1.1 -Flask==1.1.1 -google-api-core==1.16.0 -google-auth==1.12.0 +click==7.1.2 +fireo==1.2.4 +Flask==1.1.2 +google-api-core==1.17.0 +google-auth==1.14.1 google-cloud-core==1.3.0 google-cloud-firestore==1.6.2 google-cloud-logging==1.15.0 -google-cloud-pubsub==1.4.1 +google-cloud-pubsub==1.4.3 google-cloud-tasks==1.5.0 googleapis-common-protos==1.51.0 grpc-google-iam-v1==0.12.3 -grpcio==1.27.2 +grpcio==1.28.1 idna==2.9 isort==4.3.21 itsdangerous==1.1.0 -Jinja2==2.11.1 +Jinja2==2.11.2 lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 -numpy==1.18.2 -opencv-python==4.2.0.32 +numpy==1.18.3 +opencv-python==4.2.0.34 protobuf==3.11.3 pyasn1==0.4.8 pyasn1-modules==0.2.8 -pylint==2.4.4 -pytz==2019.3 +pylint==2.5.0 +pytz==2020.1 requests==2.23.0 rsa==4.0 six==1.14.0 tabulate==0.8.7 -urllib3==1.25.8 -Werkzeug==1.0.0 +toml==0.10.0 +urllib3==1.25.9 +Werkzeug==1.0.1 wrapt==1.12.1