migrated to fireo ORM

This commit is contained in:
aj 2020-04-30 14:54:05 +01:00
parent a47c1556f9
commit bc1b59c7bd
22 changed files with 439 additions and 1461 deletions

View File

@ -3,13 +3,11 @@ from flask import Blueprint, jsonify
import logging import logging
from datetime import datetime from datetime import datetime
from google.cloud import firestore
from google.cloud import tasks_v2 from google.cloud import tasks_v2
from music.api.decorators import login_or_basic_auth, admin_required from music.api.decorators import login_or_basic_auth, admin_required
blueprint = Blueprint('admin-api', __name__) blueprint = Blueprint('admin-api', __name__)
db = firestore.Client()
tasker = tasks_v2.CloudTasksClient() tasker = tasks_v2.CloudTasksClient()
task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions')
@ -20,7 +18,7 @@ logger = logging.getLogger(__name__)
@blueprint.route('/tasks', methods=['GET']) @blueprint.route('/tasks', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@admin_required @admin_required
def get_tasks(username=None): def get_tasks(user=None):
tasks = [i for i in tasker.list_tasks(task_path)] tasks = [i for i in tasker.list_tasks(task_path)]

View File

@ -1,58 +1,55 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from google.cloud import firestore
from werkzeug.security import generate_password_hash
import os import os
import json import json
import logging import logging
from datetime import datetime 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.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, \ from music.cloud.tasks import execute_all_user_playlists, execute_user_playlists, create_run_user_playlist_task, \
create_play_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.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.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 import music.db.database as database
blueprint = Blueprint('api', __name__) blueprint = Blueprint('api', __name__)
db = firestore.Client() db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@blueprint.route('/playlists', methods=['GET']) @blueprint.route('/playlists', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def get_playlists(username=None): def get_playlists(user=None):
assert user is not None
return jsonify({ 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 }), 200
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE']) @blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE'])
@login_or_basic_auth @login_or_basic_auth
def playlist(username=None): def playlist_route(user=None):
user_playlists = database.get_user_playlists(username)
user_ref = database.get_user(username).db_ref
playlists = user_ref.collection(u'playlists')
if request.method == 'GET' or request.method == 'DELETE': if request.method == 'GET' or request.method == 'DELETE':
playlist_name = request.args.get('name', None) playlist_name = request.args.get('name', None)
if playlist_name: 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 playlist is None:
return jsonify({'error': f'playlist {playlist_name} not found'}), 404
if queried_playlist is None:
return jsonify({'error': 'no playlist found'}), 404
if request.method == "GET": if request.method == "GET":
return jsonify(queried_playlist.to_dict()), 200 return jsonify(playlist.to_dict()), 200
elif request.method == 'DELETE': 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 return jsonify({"message": 'playlist deleted', "status": "success"}), 200
else: else:
@ -75,9 +72,9 @@ def playlist(username=None):
if request_json['playlist_references'] != -1: if request_json['playlist_references'] != -1:
for i in request_json['playlist_references']: 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: if updating_playlist is not None:
playlist_references.append(updating_playlist.db_ref) playlist_references.append(db.document(updating_playlist.key))
else: else:
return jsonify({"message": f'managed playlist {i} not found', "status": "error"}), 400 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_range = request_json.get('chart_range', None)
playlist_chart_limit = request_json.get('chart_limit', 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 request.method == 'PUT':
if len(queried_playlist) != 0: if playlist is not None:
return jsonify({'error': 'playlist already exists'}), 400 return jsonify({'error': 'playlist already exists'}), 400
from music.tasks.create_playlist import create_playlist as create_playlist from music.tasks.create_playlist import create_playlist as create_playlist
to_add = { new_db_playlist = Playlist(parent=user.key)
'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(),
'lastfm_stat_count': 0, new_db_playlist.name = playlist_name
'lastfm_stat_album_count': 0, new_db_playlist.parts = playlist_parts
'lastfm_stat_artist_count': 0, new_db_playlist.playlist_references = playlist_references
'lastfm_stat_percent': 0, new_db_playlist.include_library_tracks = playlist_library_tracks
'lastfm_stat_album_percent': 0, new_db_playlist.include_recommendations = playlist_recommendation
'lastfm_stat_artist_percent': 0, 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_db_playlist.type = playlist_type
new_playlist = create_playlist(username, playlist_name) new_db_playlist.last_updated = datetime.utcnow()
to_add['uri'] = str(new_playlist.uri) if new_playlist is not None else None new_db_playlist.lastfm_stat_last_refresh = datetime.utcnow()
if playlist_type == 'recents': new_db_playlist.day_boundary = playlist_day_boundary
to_add['day_boundary'] = playlist_day_boundary if playlist_day_boundary is not None else 21 new_db_playlist.add_this_month = playlist_add_this_month
to_add['add_this_month'] = playlist_add_this_month if playlist_add_this_month is not None else False new_db_playlist.add_last_month = playlist_add_last_month
to_add['add_last_month'] = playlist_add_last_month if playlist_add_last_month is not None else False
if playlist_type == 'fmchart': new_db_playlist.chart_range = playlist_chart_range
to_add['chart_range'] = playlist_chart_range new_db_playlist.chart_limit = playlist_chart_limit
to_add['chart_limit'] = playlist_chart_limit if playlist_chart_limit is not None else 50
playlists.document().set(to_add) if user.spotify_linked:
logger.info(f'added {username} / {playlist_name}') 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 return jsonify({"message": 'playlist added', "status": "success"}), 201
elif request.method == 'POST': elif request.method == 'POST':
if len(queried_playlist) == 0: if playlist is None:
return jsonify({'error': "playlist doesn't exist"}), 400 return jsonify({'error': "playlist doesn't exist"}), 400
if len(queried_playlist) > 1: updating_playlist = Playlist.collection.parent(user.key).filter('name', '==', playlist_name).get()
return jsonify({'error': "multiple playlists exist"}), 500
updating_playlist = database.get_playlist(username=username, name=playlist_name)
dic = {}
if playlist_parts is not None: if playlist_parts is not None:
if playlist_parts == -1: if playlist_parts == -1:
dic['parts'] = [] updating_playlist.parts = []
else: else:
dic['parts'] = playlist_parts updating_playlist.parts = playlist_parts
if playlist_references is not None: if playlist_references is not None:
if playlist_references == -1: if playlist_references == -1:
dic['playlist_references'] = [] updating_playlist.playlist_references = []
else: else:
dic['playlist_references'] = playlist_references updating_playlist.playlist_references = playlist_references
if playlist_uri is not None: if playlist_uri is not None:
dic['uri'] = playlist_uri updating_playlist.uri = playlist_uri
if playlist_shuffle is not None: if playlist_shuffle is not None:
dic['shuffle'] = playlist_shuffle updating_playlist.shuffle = playlist_shuffle
if playlist_day_boundary is not None: 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: 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: 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: 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: if playlist_recommendation is not None:
dic['include_recommendations'] = playlist_recommendation updating_playlist.include_recommendations = playlist_recommendation
if playlist_recommendation_sample is not None: 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: 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: 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: if playlist_type is not None:
dic['type'] = playlist_type # TODO check acceptable value
updating_playlist.type = playlist_type
if playlist_type == 'fmchart': updating_playlist.update()
dic['chart_range'] = 'YEAR' logger.info(f'updated {user.username} / {playlist_name}')
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}')
return jsonify({"message": 'playlist updated', "status": "success"}), 200 return jsonify({"message": 'playlist updated', "status": "success"}), 200
@blueprint.route('/user', methods=['GET', 'POST']) @blueprint.route('/user', methods=['GET', 'POST'])
@login_or_basic_auth @login_or_basic_auth
def user(username=None): def user_route(user=None):
assert user is not None
if request.method == 'GET': if request.method == 'GET':
return jsonify(user.to_dict()), 200
database_user = database.get_user(username) else: # POST
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
request_json = request.get_json() request_json = request.get_json()
if 'username' in request_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: if 'locked' in request_json:
logger.info(f'updating lock {username} / {request_json["locked"]}') if user.type == "admin":
actionable_user.locked = request_json['locked'] logger.info(f'updating lock {user.username} / {request_json["locked"]}')
user.locked = request_json['locked']
if 'spotify_linked' in request_json: if 'spotify_linked' in request_json:
logger.info(f'deauthing {username}') logger.info(f'deauthing {user.username}')
if request_json['spotify_linked'] is False: if request_json['spotify_linked'] is False:
actionable_user.update_database({ user.access_token = None
'access_token': None, user.refresh_token = None
'refresh_token': None, user.spotify_linked = False
'spotify_linked': False
})
if 'lastfm_username' in request_json: if 'lastfm_username' in request_json:
logger.info(f'updating lastfm username {username} -> {request_json["lastfm_username"]}') logger.info(f'updating lastfm username {user.username} -> {request_json["lastfm_username"]}')
actionable_user.lastfm_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 return jsonify({'message': 'account updated', 'status': 'succeeded'}), 200
@ -269,16 +241,15 @@ def user(username=None):
@blueprint.route('/users', methods=['GET']) @blueprint.route('/users', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@admin_required @admin_required
def users(username=None): def users(user=None):
return jsonify({ return jsonify({
'accounts': [i.to_dict() for i in database.get_users()] 'accounts': [i.to_dict() for i in User.collection.fetch()]
}), 200 }), 200
@blueprint.route('/user/password', methods=['POST']) @blueprint.route('/user/password', methods=['POST'])
@login_required @login_required
def change_password(username=None): def change_password(user=None):
request_json = request.get_json() request_json = request.get_json()
if 'new_password' in request_json and 'current_password' in request_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: if len(request_json['new_password']) > 30:
return jsonify({"error": 'password too long'}), 400 return jsonify({"error": 'password too long'}), 400
db_user = database.get_user(username) if user.check_password(request_json['current_password']):
if db_user.check_password(request_json['current_password']): user.password = generate_password_hash(request_json['new_password'])
db_user.password = request_json['new_password'] user.update()
logger.info(f'password udpated {username}') logger.info(f'password udpated {user.username}')
return jsonify({"message": 'password changed', "status": "success"}), 200 return jsonify({"message": 'password changed', "status": "success"}), 200
else: else:
logger.warning(f"incorrect password {username}") logger.warning(f"incorrect password {user.username}")
return jsonify({'error': 'wrong password provided'}), 401 return jsonify({'error': 'wrong password provided'}), 401
else: else:
@ -305,7 +276,7 @@ def change_password(username=None):
@blueprint.route('/playlist/play', methods=['POST']) @blueprint.route('/playlist/play', methods=['POST'])
@login_or_basic_auth @login_or_basic_auth
def play_playlist(username=None): def play_playlist(user=None):
request_json = request.get_json() request_json = request.get_json()
@ -321,12 +292,12 @@ def play_playlist(username=None):
request_device_name = request_json.get('device_name', 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 (request_parts and len(request_parts) > 0) or (request_playlists and len(request_playlists) > 0):
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
create_play_user_playlist_task(username, create_play_user_playlist_task(user.username,
parts=request_parts, parts=request_parts,
playlist_type=request_playlist_type, playlist_type=request_playlist_type,
playlists=request_playlists, playlists=request_playlists,
@ -338,7 +309,7 @@ def play_playlist(username=None):
add_last_month=request_add_last_month, add_last_month=request_add_last_month,
device_name=request_device_name) device_name=request_device_name)
else: else:
play_user_playlist(username, play_user_playlist(user.username,
parts=request_parts, parts=request_parts,
playlist_type=request_playlist_type, playlist_type=request_playlist_type,
playlists=request_playlists, playlists=request_playlists,
@ -352,7 +323,7 @@ def play_playlist(username=None):
return jsonify({'message': 'execution requested', 'status': 'success'}), 200 return jsonify({'message': 'execution requested', 'status': 'success'}), 200
else: else:
logger.error(f'no playlists/parts {username}') logger.error(f'no playlists/parts {user.username}')
return jsonify({'error': 'insufficient playlist sources'}), 400 return jsonify({'error': 'insufficient playlist sources'}), 400
@ -381,16 +352,16 @@ def play_playlist_task():
@blueprint.route('/playlist/run', methods=['GET']) @blueprint.route('/playlist/run', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def run_playlist(username=None): def run_playlist(user=None):
playlist_name = request.args.get('name', None) playlist_name = request.args.get('name', None)
if playlist_name: if playlist_name:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': 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: else:
run_user_playlist(username, playlist_name) run_user_playlist(user.username, playlist_name)
return jsonify({'message': 'execution requested', 'status': 'success'}), 200 return jsonify({'message': 'execution requested', 'status': 'success'}), 200
@ -416,13 +387,12 @@ def run_playlist_task():
@blueprint.route('/playlist/run/user', methods=['GET']) @blueprint.route('/playlist/run/user', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def run_user(username=None): def run_user(user=None):
db_user = database.get_user(username) if user.type == 'admin':
if db_user.user_type == db_user.Type.admin: user_name = request.args.get('username', user.username)
user_name = request.args.get('username', username)
else: else:
user_name = username user_name = user.username
execute_user_playlists(user_name) execute_user_playlists(user_name)
@ -442,7 +412,7 @@ def run_user_task():
@blueprint.route('/playlist/run/users', methods=['GET']) @blueprint.route('/playlist/run/users', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@admin_required @admin_required
def run_users(username=None): def run_users(user=None):
execute_all_user_playlists() execute_all_user_playlists()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200 return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@ -458,18 +428,18 @@ def run_users_cron():
@blueprint.route('/playlist/image', methods=['GET']) @blueprint.route('/playlist/image', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def image(username=None): def image(user=None):
name = request.args.get('name', None) name = request.args.get('name', None)
if name is None: if name is None:
return jsonify({'error': "no name provided"}), 400 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: if _playlist is None:
return jsonify({'error': "playlist not found"}), 404 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) spotify_playlist = net.get_playlist(uri_string=_playlist.uri)

View File

@ -3,7 +3,7 @@ import logging
from flask import session, request, jsonify from flask import session, request, jsonify
from music.db import database as database from music.model.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -18,17 +18,24 @@ def is_logged_in():
def is_basic_authed(): def is_basic_authed():
if request.authorization: if request.authorization:
if request.authorization.get('username', None) and request.authorization.get('password', None): if request.authorization.get('username', None) and request.authorization.get('password', None):
if database.get_user(request.authorization.username).check_password(request.authorization.password): user = User.collection.filter('username', '==', request.authorization.username.strip().lower()).get()
return True 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): def login_required(func):
@functools.wraps(func) @functools.wraps(func)
def login_required_wrapper(*args, **kwargs): def login_required_wrapper(*args, **kwargs):
if is_logged_in(): 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: else:
logger.warning('user not logged in') logger.warning('user not logged in')
return jsonify({'error': 'not logged in'}), 401 return jsonify({'error': 'not logged in'}), 401
@ -39,12 +46,15 @@ def login_or_basic_auth(func):
@functools.wraps(func) @functools.wraps(func)
def login_or_basic_auth_wrapper(*args, **kwargs): def login_or_basic_auth_wrapper(*args, **kwargs):
if is_logged_in(): if is_logged_in():
return func(username=session['username'], *args, **kwargs) user = User.collection.filter('username', '==', session['username'].strip().lower()).get()
elif is_basic_authed(): return func(user=user, *args, **kwargs)
return func(username=request.authorization.username, *args, **kwargs)
else: else:
logger.warning('user not logged in') check, user = is_basic_authed()
return jsonify({'error': 'not logged in'}), 401 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 return login_or_basic_auth_wrapper
@ -52,10 +62,10 @@ def login_or_basic_auth(func):
def admin_required(func): def admin_required(func):
@functools.wraps(func) @functools.wraps(func)
def admin_required_wrapper(*args, **kwargs): 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 is not None:
if db_user.user_type == db_user.Type.admin: if db_user.type == 'admin':
return func(*args, **kwargs) return func(*args, **kwargs)
else: else:
logger.warning(f'{db_user.username} not authorized') logger.warning(f'{db_user.username} not authorized')
@ -70,7 +80,7 @@ def admin_required(func):
def spotify_link_required(func): def spotify_link_required(func):
@functools.wraps(func) @functools.wraps(func)
def spotify_link_required_wrapper(*args, **kwargs): 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 is not None:
if db_user.spotify_linked: if db_user.spotify_linked:
@ -88,7 +98,7 @@ def spotify_link_required(func):
def lastfm_username_required(func): def lastfm_username_required(func):
@functools.wraps(func) @functools.wraps(func)
def lastfm_username_required_wrapper(*args, **kwargs): 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 is not None:
if db_user.lastfm_username and len(db_user.lastfm_username) > 0: if db_user.lastfm_username and len(db_user.lastfm_username) > 0:

View File

@ -13,9 +13,9 @@ logger = logging.getLogger(__name__)
@blueprint.route('/today', methods=['GET']) @blueprint.route('/today', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@lastfm_username_required @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()) total = net.get_scrobble_count_from_date(input_date=date.today())

View File

@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
@blueprint.route('/play', methods=['POST']) @blueprint.route('/play', methods=['POST'])
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
def play(username=None): def play(user=None):
request_json = request.get_json() request_json = request.get_json()
if 'uri' in request_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]: if uri.object_type in [Uri.ObjectType.album, Uri.ObjectType.artist, Uri.ObjectType.playlist]:
context = Context(uri) context = Context(uri)
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
player = Player(net) player = Player(net)
player.play(context=context, device_name=request_json.get('device_name', None)) player.play(context=context, device_name=request_json.get('device_name', None))
@ -39,7 +39,7 @@ def play(username=None):
except ValueError: except ValueError:
return jsonify({'error': "malformed uri provided"}), 400 return jsonify({'error': "malformed uri provided"}), 400
elif 'playlist_name' in request_json: elif 'playlist_name' in request_json:
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
playlists = net.get_playlists() playlists = net.get_playlists()
if playlists is not None: if playlists is not None:
playlist_to_play = next((i for i in playlists if i.name == request_json['playlist_name']), 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] uris = [SpotifyTrack.wrap(uri=i) for i in uris if i.object_type == Uri.ObjectType.track]
if len(uris) > 0: if len(uris) > 0:
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
player = Player(net) player = Player(net)
player.play(tracks=uris, device_name=request_json.get('device_name', None)) 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']) @blueprint.route('/next', methods=['POST'])
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
def next_track(username=None): def next_track(user=None):
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
player = Player(net) player = Player(net)
player.next() player.next()
@ -89,12 +89,12 @@ def next_track(username=None):
@blueprint.route('/shuffle', methods=['POST']) @blueprint.route('/shuffle', methods=['POST'])
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
def shuffle(username=None): def shuffle(user=None):
request_json = request.get_json() request_json = request.get_json()
if 'state' in request_json: if 'state' in request_json:
if isinstance(request_json['state'], bool): if isinstance(request_json['state'], bool):
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
player = Player(net) player = Player(net)
player.shuffle(state=request_json['state']) player.shuffle(state=request_json['state'])
@ -108,13 +108,13 @@ def shuffle(username=None):
@blueprint.route('/volume', methods=['POST']) @blueprint.route('/volume', methods=['POST'])
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
def volume(username=None): def volume(user=None):
request_json = request.get_json() request_json = request.get_json()
if 'volume' in request_json: if 'volume' in request_json:
if isinstance(request_json['volume'], int): if isinstance(request_json['volume'], int):
if 0 <= request_json['volume'] <= 100: if 0 <= request_json['volume'] <= 100:
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
player = Player(net) player = Player(net)
player.volume(value=request_json['volume']) player.volume(value=request_json['volume'])

View File

@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
@lastfm_username_required @lastfm_username_required
def count(username=None): def count(user=None):
uri = request.args.get('uri', None) uri = request.args.get('uri', None)
playlist_name = request.args.get('playlist_name', None) playlist_name = request.args.get('playlist_name', None)
@ -35,8 +35,8 @@ def count(username=None):
except ValueError: except ValueError:
return jsonify({'error': 'malformed uri provided'}), 401 return jsonify({'error': 'malformed uri provided'}), 401
spotnet = database.get_authed_spotify_network(username) spotnet = database.get_authed_spotify_network(user)
fmnet = database.get_authed_lastfm_network(username) fmnet = database.get_authed_lastfm_network(user)
counter = Counter(fmnet=fmnet, spotnet=spotnet) counter = Counter(fmnet=fmnet, spotnet=spotnet)
if uri: if uri:
@ -67,18 +67,18 @@ def count(username=None):
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
@lastfm_username_required @lastfm_username_required
def playlist_refresh(username=None): def playlist_refresh(user=None):
playlist_name = request.args.get('name', None) playlist_name = request.args.get('name', None)
if playlist_name: if playlist_name:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
create_refresh_playlist_task(username, playlist_name) create_refresh_playlist_task(user.username, playlist_name)
else: else:
refresh_lastfm_track_stats(username, playlist_name) refresh_lastfm_track_stats(user.username, playlist_name)
refresh_lastfm_album_stats(username, playlist_name) refresh_lastfm_album_stats(user.username, playlist_name)
refresh_lastfm_artist_stats(username, playlist_name) refresh_lastfm_artist_stats(user.username, playlist_name)
return jsonify({'message': 'execution requested', 'status': 'success'}), 200 return jsonify({'message': 'execution requested', 'status': 'success'}), 200
@ -135,7 +135,7 @@ def run_playlist_artist_task():
@blueprint.route('/playlist/refresh/users', methods=['GET']) @blueprint.route('/playlist/refresh/users', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@admin_required @admin_required
def run_users(username=None): def run_users(user=None):
execute_all_user_playlist_stats() execute_all_user_playlist_stats()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200 return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@ -149,13 +149,12 @@ def run_users_task():
@blueprint.route('/playlist/refresh/user', methods=['GET']) @blueprint.route('/playlist/refresh/user', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def run_user(username=None): def run_user(user=None):
db_user = database.get_user(username) if user.type == 'admin':
if db_user.type == db_user.Type.admin: user_name = request.args.get('username', user.username)
user_name = request.args.get('username', username)
else: else:
user_name = username user_name = user.username
execute_user_playlist_stats(user_name) execute_user_playlist_stats(user_name)

View File

@ -14,10 +14,10 @@ logger = logging.getLogger(__name__)
@blueprint.route('/sort', methods=['POST']) @blueprint.route('/sort', methods=['POST'])
@login_or_basic_auth @login_or_basic_auth
@spotify_link_required @spotify_link_required
def play(username=None): def play(user=None):
request_json = request.get_json() request_json = request.get_json()
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
engine = PlaylistEngine(net) engine = PlaylistEngine(net)
reverse = request_json.get('reverse', False) reverse = request_json.get('reverse', False)

View File

@ -2,40 +2,41 @@ from flask import Blueprint, jsonify, request
import logging import logging
import music.db.database as database
from music.api.decorators import login_or_basic_auth from music.api.decorators import login_or_basic_auth
from music.cloud.function import update_tag from music.cloud.function import update_tag
from music.model.tag import Tag
blueprint = Blueprint('task', __name__) blueprint = Blueprint('task', __name__)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@blueprint.route('/tag', methods=['GET']) @blueprint.route('/tag', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def tags(username=None): def tags(user=None):
logger.info(f'retrieving tags for {username}') logger.info(f'retrieving tags for {user.username}')
return jsonify({ 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 }), 200
@blueprint.route('/tag/<tag_id>', methods=['GET', 'PUT', 'POST', "DELETE"]) @blueprint.route('/tag/<tag_id>', methods=['GET', 'PUT', 'POST', "DELETE"])
@login_or_basic_auth @login_or_basic_auth
def tag(tag_id, username=None): def tag_route(tag_id, user=None):
if request.method == 'GET': if request.method == 'GET':
return get_tag(tag_id, username) return get_tag(tag_id, user)
elif request.method == 'PUT': elif request.method == 'PUT':
return put_tag(tag_id, username) return put_tag(tag_id, user)
elif request.method == 'POST': elif request.method == 'POST':
return post_tag(tag_id, username) return post_tag(tag_id, user)
elif request.method == 'DELETE': elif request.method == 'DELETE':
return delete_tag(tag_id, username) return delete_tag(tag_id, user)
def get_tag(tag_id, username): def get_tag(tag_id, user):
logger.info(f'retriving {tag_id} for {username}') 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: if db_tag is not None:
return jsonify({ return jsonify({
'tag': db_tag.to_dict() 'tag': db_tag.to_dict()
@ -44,10 +45,10 @@ def get_tag(tag_id, username):
return jsonify({"error": 'tag not found'}), 404 return jsonify({"error": 'tag not found'}), 404
def put_tag(tag_id, username): def put_tag(tag_id, user):
logger.info(f'updating {tag_id} for {username}') 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: if db_tag is None:
return jsonify({"error": 'tag not found'}), 404 return jsonify({"error": 'tag not found'}), 404
@ -92,34 +93,38 @@ def put_tag(tag_id, username):
db_tag.artists = artists db_tag.artists = artists
if update_required: 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 return jsonify({"message": 'tag updated', "status": "success"}), 200
def post_tag(tag_id, username): def post_tag(tag_id, user):
logger.info(f'creating {tag_id} for {username}') logger.info(f'creating {tag_id} for {user.username}')
tag_id = tag_id.replace(' ', '_') 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 return jsonify({"message": 'tag added', "status": "success"}), 201
def delete_tag(tag_id, username): def delete_tag(tag_id, user):
logger.info(f'deleting {tag_id} for {username}') 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
return jsonify({"message": 'tag deleted', "status": "success"}), 201
else:
return jsonify({"error": 'tag not deleted'}), 400
@blueprint.route('/tag/<tag_id>/update', methods=['GET']) @blueprint.route('/tag/<tag_id>/update', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
def tag_refresh(tag_id, username=None): def tag_refresh(tag_id, user=None):
logger.info(f'updating {tag_id} tag for {username}') logger.info(f'updating {tag_id} tag for {user.username}')
update_tag(username=username, tag_id=tag_id) update_tag(username=user.username, tag_id=tag_id)
return jsonify({"message": 'tag updated', "status": "success"}), 200 return jsonify({"message": 'tag updated', "status": "success"}), 200

View File

@ -1,6 +1,7 @@
from flask import Blueprint, session, flash, request, redirect, url_for, render_template from flask import Blueprint, session, flash, request, redirect, url_for, render_template
from google.cloud import firestore 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 urllib
import datetime import datetime
@ -8,8 +9,6 @@ import logging
from base64 import b64encode from base64 import b64encode
import requests import requests
import music.db.database as database
blueprint = Blueprint('authapi', __name__) blueprint = Blueprint('authapi', __name__)
db = firestore.Client() db = firestore.Client()
@ -31,8 +30,7 @@ def login():
flash('malformed request') flash('malformed request')
return redirect(url_for('index')) return redirect(url_for('index'))
username = username.lower() user = User.collection.filter('username', '==', username.strip().lower()).get()
user = database.get_user(username)
if user is None: if user is None:
flash('user not found') flash('user not found')
@ -45,6 +43,7 @@ def login():
return redirect(url_for('index')) return redirect(url_for('index'))
user.last_login = datetime.datetime.utcnow() user.last_login = datetime.datetime.utcnow()
user.update()
logger.info(f'success {username}') logger.info(f'success {username}')
session['username'] = username session['username'] = username
@ -91,12 +90,17 @@ def register():
flash('password mismatch') flash('password mismatch')
return redirect('authapi.register') return redirect('authapi.register')
if username in [i.to_dict()['username'] for i in if username in [i.username for i in
db.collection(u'spotify_users').where(u'username', u'==', username).stream()]: User.collection.fetch()]:
flash('username already registered') flash('username already registered')
return redirect('authapi.register') 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}') logger.info(f'new user {username}')
session['username'] = username session['username'] = username
@ -150,15 +154,15 @@ def token():
resp = req.json() resp = req.json()
user = database.get_user(session['username']) user = User.collection.filter('username', '==', session['username'].strip().lower()).get()
user.update_database({ user.access_token = resp['access_token']
'access_token': resp['access_token'], user.refresh_token = resp['refresh_token']
'refresh_token': resp['refresh_token'], user.last_refreshed = datetime.datetime.now(datetime.timezone.utc)
'last_refreshed': datetime.datetime.now(datetime.timezone.utc), user.token_expiry = resp['expires_in']
'token_expiry': resp['expires_in'], user.spotify_linked = True
'spotify_linked': True
}) user.update()
else: else:
flash('http error on token request') flash('http error on token request')
@ -174,15 +178,15 @@ def deauth():
if 'username' in session: if 'username' in session:
user = database.get_user(session['username']) user = User.collection.filter('username', '==', session['username'].strip().lower()).get()
user.update_database({ user.access_token = None
'access_token': None, user.refresh_token = None
'refresh_token': None, user.last_refreshed = datetime.datetime.now(datetime.timezone.utc)
'last_refreshed': datetime.datetime.now(datetime.timezone.utc), user.token_expiry = None
'token_expiry': None, user.spotify_linked = False
'spotify_linked': False
}) user.update()
return redirect('/app/settings/spotify') return redirect('/app/settings/spotify')

View File

@ -10,6 +10,9 @@ from music.db import database as database
from music.tasks.run_user_playlist import run_user_playlist from music.tasks.run_user_playlist import run_user_playlist
from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats 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() tasker = tasks_v2.CloudTasksClient()
task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions')
@ -21,7 +24,7 @@ def execute_all_user_playlists():
seconds_delay = 0 seconds_delay = 0
logger.info('running') 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: if iter_user.spotify_linked and not iter_user.locked:
@ -45,14 +48,18 @@ def execute_all_user_playlists():
def execute_user_playlists(username): 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 seconds_delay = 0
logger.info(f'running {username}') logger.info(f'running {username}')
for iterate_playlist in playlists: for iterate_playlist in playlists:
if iterate_playlist.uri: if iterate_playlist.uri is not None:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
create_run_user_playlist_task(username, iterate_playlist.name, seconds_delay) create_run_user_playlist_task(username, iterate_playlist.name, seconds_delay)
@ -139,7 +146,7 @@ def execute_all_user_playlist_stats():
seconds_delay = 0 seconds_delay = 0
logger.info('running') 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 \ if iter_user.spotify_linked and iter_user.lastfm_username and \
len(iter_user.lastfm_username) > 0 and not iter_user.locked: 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): def execute_user_playlist_stats(username):
playlists = database.get_user_playlists(username) user = User.collection.filter('username', '==', username.strip().lower()).get()
user = database.get_user(username) if user is None:
logger.error(f'user {username} not found')
playlists = Playlist.collection.parent(user.key).fetch()
seconds_delay = 0 seconds_delay = 0
logger.info(f'running {username}') logger.info(f'running {username}')
if user.lastfm_username and len(user.lastfm_username) > 0: if user.lastfm_username and len(user.lastfm_username) > 0:
for playlist in playlists: for playlist in playlists:
if playlist.uri: if playlist.uri is not None:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
create_refresh_playlist_task(username, playlist.name, seconds_delay) create_refresh_playlist_task(username, playlist.name, seconds_delay)

View File

@ -1,15 +1,11 @@
from google.cloud import firestore from google.cloud import firestore
import logging import logging
from datetime import timedelta, datetime, timezone 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 spotframework.net.network import Network as SpotifyNetwork
from fmframework.net.network import Network as FmNetwork from fmframework.net.network import Network as FmNetwork
from music.db.user import DatabaseUser from music.db.user import DatabaseUser
from music.model.user import User from music.model.user import User
from music.model.playlist import Playlist, RecentsPlaylist, LastFMChartPlaylist, Sort
from music.model.tag import Tag
db = firestore.Client() db = firestore.Client()
@ -18,22 +14,23 @@ logger = logging.getLogger(__name__)
def refresh_token_database_callback(user): def refresh_token_database_callback(user):
if isinstance(user, DatabaseUser): 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') logger.debug(f'{user.user_id} database entry updated')
else: else:
logger.error('user has no attached id') logger.error('user has no attached id')
def get_authed_spotify_network(username): def get_authed_spotify_network(user):
user = get_user(username)
if user is not None: if user is not None:
if user.spotify_linked: if user.spotify_linked:
spotify_keys = db.document('key/spotify').get().to_dict() 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'], user_obj = DatabaseUser(client_id=spotify_keys['clientid'],
client_secret=spotify_keys['clientsecret'], client_secret=spotify_keys['clientsecret'],
refresh_token=user.refresh_token, refresh_token=user.refresh_token,
user_id=username, user_id=user.username,
access_token=user.access_token) access_token=user.access_token)
user_obj.on_refresh.append(refresh_token_database_callback) user_obj.on_refresh.append(refresh_token_database_callback)
@ -54,403 +51,15 @@ def get_authed_spotify_network(username):
else: else:
logger.error('user spotify not linked') logger.error('user spotify not linked')
else: else:
logger.error(f'user {username} not found') logger.error(f'user {user.username} not found')
def get_authed_lastfm_network(username): def get_authed_lastfm_network(user):
user = get_user(username)
if user: if user:
if user.lastfm_username: if user.lastfm_username:
fm_keys = db.document('key/fm').get().to_dict() fm_keys = db.document('key/fm').get().to_dict()
return FmNetwork(username=user.lastfm_username, api_key=fm_keys['clientid']) return FmNetwork(username=user.lastfm_username, api_key=fm_keys['clientid'])
else: else:
logger.error(f'{username} has no last.fm username') logger.error(f'{user.username} has no last.fm username')
else: else:
logger.error(f'user {username} not found') logger.error(f'user {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
})

View File

@ -1,9 +1,7 @@
from google.cloud import firestore
import music.db.database as database
from music.model.user import User from music.model.user import User
from music.model.playlist import Playlist
import logging import logging
db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,7 +14,7 @@ class PartGenerator:
if user: if user:
self.user = user self.user = user
elif username: elif username:
pulled_user = database.get_user(username) pulled_user = User.collection.filter('username', '==', username.strip().lower()).get()
if pulled_user: if pulled_user:
self.user = pulled_user self.user = pulled_user
else: else:
@ -38,13 +36,14 @@ class PartGenerator:
def process_reference_by_name(self, name): 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 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.parts += playlist.parts
self.queried_playlists.append(playlist.id)
for i in playlist.playlist_references: for i in playlist.playlist_references:
if i.id not in self.queried_playlists: if i.id not in self.queried_playlists:
@ -61,6 +60,7 @@ class PartGenerator:
if ref.id not in self.queried_playlists: if ref.id not in self.queried_playlists:
playlist_reference_object = ref.get().to_dict() playlist_reference_object = ref.get().to_dict()
self.parts += playlist_reference_object['parts'] self.parts += playlist_reference_object['parts']
self.queried_playlists.append(ref.id)
for i in playlist_reference_object['playlist_references']: for i in playlist_reference_object['playlist_references']:
self.process_reference_by_reference(i) self.process_reference_by_reference(i)

View File

@ -1,11 +1,7 @@
from typing import List
from enum import Enum from enum import Enum
from datetime import datetime
from google.cloud.firestore import DocumentReference
from fmframework.net.network import Network from fireo.models import Model
from fireo.fields import TextField, BooleanField, DateTime, NumberField, ListField
import music.db.database as database
class Sort(Enum): class Sort(Enum):
@ -14,466 +10,52 @@ class Sort(Enum):
release_date = 3 release_date = 3
class Playlist: class Playlist(Model):
def __init__(self, class Meta:
uri: str, collection_name = 'playlists'
name: str,
username: str,
db_ref: DocumentReference, uri = TextField()
name = TextField(required=True)
type = TextField(required=True)
include_recommendations: bool, include_recommendations = BooleanField(default=False)
recommendation_sample: int, recommendation_sample = NumberField(default=10)
include_library_tracks: bool, include_library_tracks = BooleanField(default=False)
parts: List[str], parts = ListField(default=[])
playlist_references: List[DocumentReference], playlist_references = ListField(default=[])
shuffle: bool, shuffle = BooleanField(default=False)
sort: Sort = None, sort = TextField(default='release_date')
description_overwrite = TextField()
description_suffix = TextField()
description_overwrite: str = None, last_updated = DateTime()
description_suffix: str = None,
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_percent = NumberField(default=0)
lastfm_stat_album_count: int = None, lastfm_stat_album_percent = NumberField(default=0)
lastfm_stat_artist_count: int = None, lastfm_stat_artist_percent = NumberField(default=0)
lastfm_stat_percent: int = None, lastfm_stat_last_refresh = DateTime()
lastfm_stat_album_percent: int = None,
lastfm_stat_artist_percent: int = None,
lastfm_stat_last_refresh: datetime = None): add_last_month = BooleanField(default=False)
self._uri = uri add_this_month = BooleanField(default=False)
self.name = name day_boundary = NumberField(default=21)
self.username = username
self.db_ref = db_ref chart_range = TextField(default='1month')
chart_limit = NumberField(default=50)
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
def to_dict(self): def to_dict(self):
return { to_return = super().to_dict()
'uri': self.uri,
'name': self.name,
'type': 'default',
'include_recommendations': self.include_recommendations, to_return["playlist_references"] = [i.get().to_dict().get('name') for i in to_return['playlist_references']]
'recommendation_sample': self.recommendation_sample,
'include_library_tracks': self.include_library_tracks,
'parts': self.parts, # remove unnecessary and sensitive fields
'playlist_references': [i.get().to_dict().get('name') for i in self.playlist_references], to_return.pop('id', None)
'shuffle': self.shuffle, to_return.pop('key', None)
'sort': self.sort.name, return to_return
'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

View File

@ -1,129 +1,30 @@
from datetime import datetime from fireo.models import Model
import music.db.database as db from fireo.fields import TextField, DateTime, NumberField, ListField
class Tag: class Tag(Model):
class Meta:
collection_name = 'tags'
def __init__(self, tag_id = TextField(required=True)
tag_id: str, name = TextField(required=True)
name: str, username = TextField(required=True)
username: str,
db_ref, tracks = ListField(default=[])
albums = ListField(default=[])
artists = ListField(default=[])
tracks, count = NumberField(default=0)
albums, proportion = NumberField(default=0)
artists, total_user_scrobbles = NumberField(default=0)
count: int, last_updated = DateTime()
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
def to_dict(self): def to_dict(self):
return { to_return = super().to_dict()
'tag_id': self.tag_id,
'name': self.name,
'username': self.username,
'tracks': self.tracks, # remove unnecessary and sensitive fields
'albums': self.albums, to_return.pop('id', None)
'artists': self.artists, to_return.pop('key', None)
'count': self.count, return to_return
'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

View File

@ -1,172 +1,42 @@
from datetime import datetime from fireo.models import Model
from enum import Enum from fireo.fields import TextField, BooleanField, DateTime, NumberField
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import check_password_hash
import music.db.database as database
class User: class User(Model):
class Type(Enum): class Meta:
user = 1 collection_name = 'spotify_users'
admin = 2
def __init__(self, username = TextField(required=True)
username: str, password = TextField(required=True)
password: str, email = TextField()
db_ref, type = TextField(default="user")
email: str,
user_type: Type,
last_login: datetime,
last_refreshed: datetime,
locked: bool,
validated: bool,
spotify_linked: bool, last_login = DateTime()
access_token: str, last_refreshed = DateTime()
refresh_token: str, locked = BooleanField(default=False, required=True)
token_expiry: int, validated = BooleanField(default=True, required=True)
lastfm_username: str = None): spotify_linked = BooleanField(default=False, required=True)
self.username = username access_token = TextField()
self._password = password refresh_token = TextField()
self.db_ref = db_ref token_expiry = NumberField()
self._email = email
self._type = user_type
self._last_login = last_login lastfm_username = TextField()
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
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password, password) return check_password_hash(self.password, password)
def to_dict(self): def to_dict(self):
return { to_return = super().to_dict()
'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
}
def update_database(self, updates): # remove unnecessary and sensitive fields
database.update_user(username=self.username, updates=updates) 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 return to_return
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

View File

@ -9,21 +9,19 @@ db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_playlist(username, name): def create_playlist(user, name):
logger.info(f'creating spotify playlist for {username} / {name}') logger.info(f'creating spotify playlist for {user.username} / {name}')
user = database.get_user(username)
if user is not None: 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) playlist = net.create_playlist(net.user.username, name)
if playlist is not None: if playlist is not None:
return playlist return playlist
else: else:
logger.error(f'no response received {username} / {name}') logger.error(f'no response received {user.username} / {name}')
return return
else: else:
logger.error(f'{username} not found') logger.error(f'{user.username} not provided')
return

View File

@ -1,5 +1,3 @@
from google.cloud import firestore
import datetime import datetime
import logging import logging
@ -11,8 +9,7 @@ from spotframework.engine.processor.deduplicate import DeduplicateByID
from spotframework.player.player import Player from spotframework.player.player import Player
import music.db.database as database import music.db.database as database
from music.db.part_generator import PartGenerator from music.db.part_generator import PartGenerator
from music.model.user import User
db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,7 +26,11 @@ def play_user_playlist(username,
add_last_month=False, add_last_month=False,
device_name=None): 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}') 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})') logger.critical(f'no playlists to use for creation ({username})')
return None return None
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
device = None device = None
if device_name: if device_name:

View File

@ -4,6 +4,8 @@ import logging
from datetime import datetime from datetime import datetime
import music.db.database as database 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 spotfm.maths.counter import Counter
from spotframework.model.uri import Uri 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}') logger.info(f'refreshing {playlist_name} stats for {username}')
fmnet = database.get_authed_lastfm_network(username=username) user = User.collection.filter('username', '==', username.strip().lower()).get()
spotnet = database.get_authed_spotify_network(username=username) 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) 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: if playlist is None:
logger.critical(f'playlist {playlist_name} for {username} not found') logger.critical(f'playlist {playlist_name} for {username} not found')
@ -40,23 +46,26 @@ def refresh_lastfm_track_stats(username, playlist_name):
else: else:
percent = 0 percent = 0
playlist.update_database({ playlist.lastfm_stat_count = track_count
'lastfm_stat_count': track_count, playlist.lastfm_stat_percent = percent
'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): def refresh_lastfm_album_stats(username, playlist_name):
logger.info(f'refreshing {playlist_name} stats for {username}') logger.info(f'refreshing {playlist_name} stats for {username}')
fmnet = database.get_authed_lastfm_network(username=username) user = User.collection.filter('username', '==', username.strip().lower()).get()
spotnet = database.get_authed_spotify_network(username=username) 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) 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: if playlist is None:
logger.critical(f'playlist {playlist_name} for {username} not found') logger.critical(f'playlist {playlist_name} for {username} not found')
@ -75,23 +84,26 @@ def refresh_lastfm_album_stats(username, playlist_name):
else: else:
album_percent = 0 album_percent = 0
playlist.update_database({ playlist.lastfm_stat_album_count = album_count
'lastfm_stat_album_count': album_count, playlist.lastfm_stat_album_percent = album_percent
'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): def refresh_lastfm_artist_stats(username, playlist_name):
logger.info(f'refreshing {playlist_name} stats for {username}') logger.info(f'refreshing {playlist_name} stats for {username}')
fmnet = database.get_authed_lastfm_network(username=username) user = User.collection.filter('username', '==', username.strip().lower()).get()
spotnet = database.get_authed_spotify_network(username=username) 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) 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: if playlist is None:
logger.critical(f'playlist {playlist_name} for {username} not found') logger.critical(f'playlist {playlist_name} for {username} not found')
@ -110,9 +122,8 @@ def refresh_lastfm_artist_stats(username, playlist_name):
else: else:
artist_percent = 0 artist_percent = 0
playlist.update_database({ playlist.lastfm_stat_artist_count = artist_count
'lastfm_stat_artist_count': artist_count, playlist.lastfm_stat_artist_percent = artist_percent
'lastfm_stat_artist_percent': artist_percent, playlist.lastfm_stat_last_refresh = datetime.utcnow()
'lastfm_stat_last_refresh': datetime.utcnow() playlist.update()
})

View File

@ -14,7 +14,8 @@ from spotfm.engine.chart_source import ChartSource
import music.db.database as database import music.db.database as database
from music.db.part_generator import PartGenerator 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() db = firestore.Client()
@ -23,7 +24,9 @@ logger = logging.getLogger(__name__)
def run_user_playlist(username, playlist_name): def run_user_playlist(username, playlist_name):
"""Generate and upadate a user's playlist""" """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}') logger.info(f'running {username} / {playlist_name}')
@ -32,7 +35,7 @@ def run_user_playlist(username, playlist_name):
logger.critical(f'{username} not found') logger.critical(f'{username} not found')
return 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: if playlist is None:
logger.critical(f'playlist not found ({username}/{playlist_name})') logger.critical(f'playlist not found ({username}/{playlist_name})')
@ -44,7 +47,7 @@ def run_user_playlist(username, playlist_name):
# END CHECKS # END CHECKS
net = database.get_authed_spotify_network(username) net = database.get_authed_spotify_network(user)
engine = PlaylistEngine(net) engine = PlaylistEngine(net)
part_generator = PartGenerator(user=user) part_generator = PartGenerator(user=user)
@ -63,11 +66,11 @@ def run_user_playlist(username, playlist_name):
params.append(LibraryTrackSource.Params()) params.append(LibraryTrackSource.Params())
# END OPTIONS # END OPTIONS
if isinstance(playlist, LastFMChartPlaylist): if playlist.type == 'fmchart':
if user.lastfm_username is None: if user.lastfm_username is None:
logger.error(f'{username} has no associated last.fm username, chart source skipped') logger.error(f'{username} has no associated last.fm username, chart source skipped')
else: 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)) params.append(ChartSource.Params(chart_range=playlist.chart_range, limit=playlist.chart_limit))
else: else:
@ -78,7 +81,7 @@ def run_user_playlist(username, playlist_name):
processors.append(SortReleaseDate(reverse=True)) processors.append(SortReleaseDate(reverse=True))
# GENERATE TRACKS # GENERATE TRACKS
if isinstance(playlist, RecentsPlaylist): if playlist.type == 'recents':
boundary_date = datetime.datetime.now(datetime.timezone.utc) - \ boundary_date = datetime.datetime.now(datetime.timezone.utc) - \
datetime.timedelta(days=int(playlist.day_boundary)) datetime.timedelta(days=int(playlist.day_boundary))
tracks = engine.get_recent_playlist(params=params, tracks = engine.get_recent_playlist(params=params,
@ -101,3 +104,4 @@ def run_user_playlist(username, playlist_name):
overwrite=overwrite, overwrite=overwrite,
suffix=suffix) suffix=suffix)
playlist.last_updated = datetime.datetime.utcnow() playlist.last_updated = datetime.datetime.utcnow()
playlist.update()

View File

@ -2,6 +2,8 @@ import logging
from datetime import datetime from datetime import datetime
import music.db.database as database import music.db.database as database
from music.model.user import User
from music.model.tag import Tag
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -9,19 +11,20 @@ logger = logging.getLogger(__name__)
def update_tag(username, tag_id): def update_tag(username, tag_id):
logger.info(f'updating {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: if tag is None:
logger.error(f'{tag_id} for {username} not found') logger.error(f'{tag_id} for {username} not found')
return return
user = database.get_user(username)
if user.lastfm_username is None or len(user.lastfm_username) == 0: if user.lastfm_username is None or len(user.lastfm_username) == 0:
logger.error(f'{username} has no last.fm username') logger.error(f'{username} has no last.fm username')
return return
net = database.get_authed_lastfm_network(username=username) net = database.get_authed_lastfm_network(user)
tag_count = 0 tag_count = 0
user_scrobbles = net.get_user_scrobble_count() user_scrobbles = net.get_user_scrobble_count()
@ -60,13 +63,13 @@ def update_tag(username, tag_id):
tracks.append(track) tracks.append(track)
tag.update_database({ tag.tracks = tracks
'tracks': tracks, tag.albums = albums
'albums': albums, tag.artists = artists
'artists': artists,
'total_user_scrobbles': user_scrobbles, tag.total_user_scrobbles = user_scrobbles
'count': tag_count, tag.count = tag_count
'proportion': (tag_count / user_scrobbles) * 100, tag.proportion = (tag_count / user_scrobbles) * 100
'last_updated': datetime.utcnow() tag.last_updated = datetime.utcnow()
})
tag.update()

View File

@ -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: ') name = input('enter playlist name: ')
playlist = Playlist.collection.parent(user.key).filter('name', '==', name).get()
playlist = next((i for i in playlists if i.name == name), None)
if playlist is not None: if playlist is not None:
new_name = input('enter new name: ') new_name = input('enter new name: ')
playlist.update_database({'name': new_name}) playlist.name = new_name
playlist.update()
else: else:
print('playlist not found') print('playlist not found')

View File

@ -1,37 +1,39 @@
astroid==2.3.3 astroid==2.4.0
cachetools==4.0.0 cachetools==4.1.0
certifi==2019.11.28 certifi==2020.4.5.1
chardet==3.0.4 chardet==3.0.4
click==7.1.1 click==7.1.2
Flask==1.1.1 fireo==1.2.4
google-api-core==1.16.0 Flask==1.1.2
google-auth==1.12.0 google-api-core==1.17.0
google-auth==1.14.1
google-cloud-core==1.3.0 google-cloud-core==1.3.0
google-cloud-firestore==1.6.2 google-cloud-firestore==1.6.2
google-cloud-logging==1.15.0 google-cloud-logging==1.15.0
google-cloud-pubsub==1.4.1 google-cloud-pubsub==1.4.3
google-cloud-tasks==1.5.0 google-cloud-tasks==1.5.0
googleapis-common-protos==1.51.0 googleapis-common-protos==1.51.0
grpc-google-iam-v1==0.12.3 grpc-google-iam-v1==0.12.3
grpcio==1.27.2 grpcio==1.28.1
idna==2.9 idna==2.9
isort==4.3.21 isort==4.3.21
itsdangerous==1.1.0 itsdangerous==1.1.0
Jinja2==2.11.1 Jinja2==2.11.2
lazy-object-proxy==1.4.3 lazy-object-proxy==1.4.3
MarkupSafe==1.1.1 MarkupSafe==1.1.1
mccabe==0.6.1 mccabe==0.6.1
numpy==1.18.2 numpy==1.18.3
opencv-python==4.2.0.32 opencv-python==4.2.0.34
protobuf==3.11.3 protobuf==3.11.3
pyasn1==0.4.8 pyasn1==0.4.8
pyasn1-modules==0.2.8 pyasn1-modules==0.2.8
pylint==2.4.4 pylint==2.5.0
pytz==2019.3 pytz==2020.1
requests==2.23.0 requests==2.23.0
rsa==4.0 rsa==4.0
six==1.14.0 six==1.14.0
tabulate==0.8.7 tabulate==0.8.7
urllib3==1.25.8 toml==0.10.0
Werkzeug==1.0.0 urllib3==1.25.9
Werkzeug==1.0.1
wrapt==1.12.1 wrapt==1.12.1