admin script, live playlist updating with functions or tasks, config option

This commit is contained in:
aj 2020-06-30 16:38:06 +01:00
parent 9f330edaba
commit 2f3f22de0d
21 changed files with 285 additions and 160 deletions

110
admin Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env python3
import shutil
import os
import sys
from cmd import Cmd
stage_dir = '_playlist-manager'
scss_rel_path = os.path.join('src', 'scss', 'style.scss')
css_rel_path = os.path.join('build', 'style.css')
class Admin(Cmd):
intro = 'Music Tools Admin... ? for help'
prompt = '> '
def prepare_stage(self):
print('>> backing up a directory')
os.chdir('..')
print('>> deleting old deployment stage')
shutil.rmtree(stage_dir, ignore_errors=True)
print('>> copying main source')
shutil.copytree('playlist-manager', stage_dir)
for dependency in ['spotframework', 'fmframework', 'spotfm']:
print(f'>> injecting {dependency}')
shutil.copytree(
os.path.join(dependency, dependency),
os.path.join(stage_dir, dependency)
)
os.chdir(stage_dir)
os.system('gcloud config set project sarsooxyz')
def prepare_frontend(self):
print('>> building css')
os.system(f'sass --style=compressed {scss_rel_path} {css_rel_path}')
print('>> building javascript')
os.system('npm run build')
def prepare_main(self, path):
print('>> preparing main.py')
shutil.copy(f'main.{path}.py', 'main.py')
def deploy_function(self, name, timeout: int = 60):
os.system(f'gcloud functions deploy {name} '
f'--runtime=python38 '
f'--set-env-vars DEPLOY_DESTINATION=PROD '
f'--timeout={timeout}s')
def do_api(self, args):
self.prepare_stage()
self.prepare_frontend()
self.prepare_main('api')
print('>> deploying')
os.system('gcloud app deploy')
def do_tag(self, args):
self.prepare_stage()
self.prepare_main('update_tag')
print('>> deploying')
self.deploy_function('update_tag')
def do_playlist(self, args):
self.prepare_stage()
self.prepare_main('run_playlist')
print('>> deploying')
self.deploy_function('run_user_playlist')
def do_exit(self, args):
exit(0)
def do_sass(self, args):
os.system(f'sass --style=compressed {scss_rel_path} {css_rel_path}')
def do_watchsass(self, args):
os.system(f'sass --style=compressed --watch {scss_rel_path} {css_rel_path}')
def do_rename(self, args):
from music.model.user import User
from music.model.playlist import Playlist
username = input('enter username: ')
user = User.collection.filter('username', '==', username).get()
if user is None:
print('>> user not found')
name = input('enter playlist name: ')
playlist = Playlist.collection.parent(user.key).filter('name', '==', name).get()
if playlist is None:
print('>> playlist not found')
new_name = input('enter new name: ')
playlist.name = new_name
playlist.update()
if __name__ == '__main__':
console = Admin()
if len(sys.argv) > 1:
console.onecmd(' '.join(sys.argv[1:]))
else:
console.cmdloop()

View File

@ -1,4 +1,4 @@
runtime: python37 runtime: python38
service: spotify service: spotify
instance_class: F2 instance_class: F2

55
deploy
View File

@ -1,55 +0,0 @@
#!/usr/bin/env bash
stage_dir=_playlist-manager
echo '>> backing up a directory'
cd ..
echo '>> deleting old deployment stage'
rm -rf $stage_dir
echo '>> copying main source'
cp -r playlist-manager $stage_dir
echo '>> injecting spotframework'
cp -r spotframework/spotframework $stage_dir/
echo '>> injecting fmframework'
cp -r fmframework/fmframework $stage_dir/
echo '>> injecting spotfm'
cp -r spotfm/spotfm $stage_dir/
cd $stage_dir
gcloud config set project sarsooxyz
echo '>>> Target?'
echo ''
echo '(0) > api'
echo '(1) > update_tag'
read deploy_target
case "$deploy_target" in
0)
echo '>> building css'
sass --style=compressed src/scss/style.scss build/style.css
echo '>> building javascript'
npm run build
echo '>> deploying'
gcloud app deploy
;;
1)
echo '>> deploying update_tag'
gcloud functions deploy update_tag
;;
esac

6
main.api.py Normal file
View File

@ -0,0 +1,6 @@
from music.music import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)

15
main.run_playlist.py Normal file
View File

@ -0,0 +1,15 @@
def run_user_playlist(event, context):
import logging
logger = logging.getLogger('music')
if event.get('attributes'):
if 'username' in event['attributes'] and 'name' in event['attributes']:
from music.tasks.run_user_playlist import run_user_playlist as do_run_user_playlist
do_run_user_playlist(username=event['attributes']['username'], playlist_name=event['attributes']["name"])
else:
logger.error('no parameters in event attributes')
else:
logger.error('no attributes in event')

View File

@ -1,9 +1,3 @@
from music import app
from music.tasks.update_tag import update_tag as do_update_tag
app = app
def update_tag(event, context): def update_tag(event, context):
import logging import logging
@ -11,12 +5,11 @@ def update_tag(event, context):
if event.get('attributes'): if event.get('attributes'):
if 'username' in event['attributes'] and 'tag_id' in event['attributes']: if 'username' in event['attributes'] and 'tag_id' in event['attributes']:
from music.tasks.update_tag import update_tag as do_update_tag
do_update_tag(username=event['attributes']['username'], tag_id=event['attributes']["tag_id"]) do_update_tag(username=event['attributes']['username'], tag_id=event['attributes']["tag_id"])
else: else:
logger.error('no parameters in event attributes') logger.error('no parameters in event attributes')
else: else:
logger.error('no attributes in event') logger.error('no attributes in event')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)

View File

@ -1,5 +1,3 @@
from .music import app
import logging import logging
import os import os

View File

@ -8,7 +8,8 @@ import logging
from datetime import datetime from datetime import datetime
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 update_all_user_playlists, update_playlists, run_user_playlist_task from music.cloud import queue_run_user_playlist, offload_or_run_user_playlist
from music.cloud.tasks import update_all_user_playlists, update_playlists
from music.tasks.run_user_playlist import run_user_playlist from music.tasks.run_user_playlist import run_user_playlist
from music.model.user import User from music.model.user import User
@ -284,9 +285,9 @@ def run_playlist(user=None):
if playlist_name: if playlist_name:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
run_user_playlist_task(user.username, playlist_name) queue_run_user_playlist(user.username, playlist_name) # pass to either cloud tasks or functions
else: else:
run_user_playlist(user.username, playlist_name) run_user_playlist(user.username, playlist_name) # update synchronously
return jsonify({'message': 'execution requested', 'status': 'success'}), 200 return jsonify({'message': 'execution requested', 'status': 'success'}), 200
@ -297,7 +298,7 @@ def run_playlist(user=None):
@blueprint.route('/playlist/run/task', methods=['POST']) @blueprint.route('/playlist/run/task', methods=['POST'])
@cloud_task @cloud_task
def run_playlist_task(): def run_playlist_task(): # receives cloud tasks request for update
payload = request.get_data(as_text=True) payload = request.get_data(as_text=True)
if payload: if payload:
@ -305,7 +306,7 @@ def run_playlist_task():
logger.info(f'running {payload["username"]} / {payload["name"]}') logger.info(f'running {payload["username"]} / {payload["name"]}')
run_user_playlist(payload['username'], payload['name']) offload_or_run_user_playlist(payload['username'], payload['name']) # check whether offloading to cloud function
return jsonify({'message': 'executed playlist', 'status': 'success'}), 200 return jsonify({'message': 'executed playlist', 'status': 'success'}), 200

View File

@ -1,7 +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 werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from music.model.user import User from music.model.user import User
from music.model.config import Config
import urllib import urllib
import datetime import datetime
@ -11,8 +11,6 @@ import requests
blueprint = Blueprint('authapi', __name__) blueprint = Blueprint('authapi', __name__)
db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -98,7 +96,7 @@ def register():
user = User() user = User()
user.username = username user.username = username
user.password = generate_password_hash(password) user.password = generate_password_hash(password)
user.last_login = datetime.utcnow() user.last_login = datetime.datetime.utcnow()
user.save() user.save()
@ -112,10 +110,10 @@ def auth():
if 'username' in session: if 'username' in session:
client_id = db.document('key/spotify').get().to_dict()['clientid'] config = Config.collection.get("config/music-tools")
params = urllib.parse.urlencode( params = urllib.parse.urlencode(
{ {
'client_id': client_id, 'client_id': config.spotify_client_id,
'response_type': 'code', 'response_type': 'code',
'scope': 'playlist-modify-public playlist-modify-private playlist-read-private user-read-playback-state user-modify-playback-state user-library-read', 'scope': 'playlist-modify-public playlist-modify-private playlist-read-private user-read-playback-state user-modify-playback-state user-library-read',
'redirect_uri': 'https://music.sarsoo.xyz/auth/spotify/token' 'redirect_uri': 'https://music.sarsoo.xyz/auth/spotify/token'
@ -137,9 +135,11 @@ def token():
flash('authorization failed') flash('authorization failed')
return redirect('app_route') return redirect('app_route')
else: else:
app_credentials = db.document('key/spotify').get().to_dict() config = Config.collection.get("config/music-tools")
idsecret = b64encode(bytes(app_credentials['clientid'] + ':' + app_credentials['clientsecret'], "utf-8")).decode("ascii") idsecret = b64encode(
bytes(config.spotify_client_id + ':' + config.spotify_client_secret, "utf-8")
).decode("ascii")
headers = {'Authorization': 'Basic %s' % idsecret} headers = {'Authorization': 'Basic %s' % idsecret}
data = { data = {

View File

@ -0,0 +1,49 @@
import logging
from music.model.config import Config
from music.tasks.run_user_playlist import run_user_playlist as run_now
from .function import run_user_playlist_function
from .tasks import run_user_playlist_task
logger = logging.getLogger(__name__)
def queue_run_user_playlist(username: str, playlist_name: str):
config = Config.collection.get("config/music-tools")
if config is None:
logger.error(f'no config object returned, passing to cloud function {username} / {playlist_name}')
run_user_playlist_function(username=username, playlist_name=playlist_name)
if config.playlist_cloud_operating_mode == 'task':
logger.debug(f'passing {username} / {playlist_name} to cloud tasks')
run_user_playlist_task(username=username, playlist_name=playlist_name)
elif config.playlist_cloud_operating_mode == 'function':
logger.debug(f'passing {username} / {playlist_name} to cloud function')
run_user_playlist_function(username=username, playlist_name=playlist_name)
else:
logger.critical(f'invalid operating mode {username} / {playlist_name}, '
f'{config.playlist_cloud_operating_mode}, passing to cloud function')
run_user_playlist_function(username=username, playlist_name=playlist_name)
def offload_or_run_user_playlist(username: str, playlist_name: str):
config = Config.collection.get("config/music-tools")
if config is None:
logger.error(f'no config object returned, passing to cloud function {username} / {playlist_name}')
run_user_playlist_function(username=username, playlist_name=playlist_name)
if config.playlist_cloud_operating_mode == 'task':
run_now(username=username, playlist_name=playlist_name)
elif config.playlist_cloud_operating_mode == 'function':
logger.debug(f'offloading {username} / {playlist_name} to cloud function')
run_user_playlist_function(username=username, playlist_name=playlist_name)
else:
logger.critical(f'invalid operating mode {username} / {playlist_name}, '
f'{config.playlist_cloud_operating_mode}, passing to cloud function')
run_user_playlist_function(username=username, playlist_name=playlist_name)

View File

@ -9,12 +9,27 @@ def update_tag(username, tag_id):
"""Queue serverless tag update for user""" """Queue serverless tag update for user"""
logger.info(f'queuing {tag_id} update for {username}') logger.info(f'queuing {tag_id} update for {username}')
if username is None: if username is None or tag_id is None:
logger.error(f'no username provided') logger.error(f'less than two args provided, {username} / {tag_id}')
return return
if tag_id is None: if not isinstance(username, str) or not isinstance(tag_id, str):
logger.error(f'no tag_id provided for {username}') logger.error(f'less than two strings provided, {type(username)} / {type(tag_id)}')
return return
publisher.publish('projects/sarsooxyz/topics/update_tag', b'', tag_id=tag_id, username=username) publisher.publish('projects/sarsooxyz/topics/update_tag', b'', tag_id=tag_id, username=username)
def run_user_playlist_function(username, playlist_name):
"""Queue serverless playlist update for user"""
logger.info(f'queuing {playlist_name} update for {username}')
if username is None or playlist_name is None:
logger.error(f'less than two args provided, {username} / {playlist_name}')
return
if not isinstance(username, str) or not isinstance(playlist_name, str):
logger.error(f'less than two strings provided, {type(username)} / {type(playlist_name)}')
return
publisher.publish('projects/sarsooxyz/topics/run_user_playlist', b'', name=playlist_name, username=username)

View File

@ -1,13 +1,12 @@
from google.cloud import firestore from dataclasses import dataclass
import logging import logging
from datetime import timedelta, datetime, timezone from datetime import timedelta, datetime, timezone
from spotframework.net.network import Network as SpotifyNetwork, SpotifyNetworkException from spotframework.net.network import Network as SpotifyNetwork, SpotifyNetworkException
from spotframework.net.user import NetworkUser
from fmframework.net.network import Network as FmNetwork from fmframework.net.network import Network as FmNetwork
from music.db.user import DatabaseUser
from music.model.user import User from music.model.user import User
from music.model.config import Config
db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,10 +32,10 @@ def refresh_token_database_callback(user):
def get_authed_spotify_network(user): def get_authed_spotify_network(user):
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() config = Config.collection.get("config/music-tools")
user_obj = DatabaseUser(client_id=spotify_keys['clientid'], user_obj = DatabaseUser(client_id=config.spotify_client_id,
client_secret=spotify_keys['clientsecret'], client_secret=config.spotify_client_secret,
refresh_token=user.refresh_token, refresh_token=user.refresh_token,
user_id=user.username, user_id=user.username,
access_token=user.access_token) access_token=user.access_token)
@ -66,9 +65,15 @@ def get_authed_spotify_network(user):
def get_authed_lastfm_network(user): def get_authed_lastfm_network(user):
if user is not None: if user is not None:
if user.lastfm_username: if user.lastfm_username:
fm_keys = db.document('key/fm').get().to_dict() config = Config.collection.get("config/music-tools")
return FmNetwork(username=user.lastfm_username, api_key=fm_keys['clientid']) return FmNetwork(username=user.lastfm_username, api_key=config.last_fm_client_id)
else: else:
logger.error(f'{user.username} has no last.fm username') logger.error(f'{user.username} has no last.fm username')
else: else:
logger.error(f'no user provided') logger.error(f'no user provided')
@dataclass
class DatabaseUser(NetworkUser):
"""adding music tools username to spotframework network user"""
user_id: str = None

View File

@ -1,14 +0,0 @@
from spotframework.util.console import Color
from spotframework.net.user import NetworkUser
class DatabaseUser(NetworkUser):
def __init__(self, client_id, client_secret, refresh_token, user_id, access_token=None):
super().__init__(client_id=client_id, client_secret=client_secret,
refresh_token=refresh_token, access_token=access_token)
self.user_id = user_id
def __repr__(self):
return Color.RED + Color.BOLD + 'DatabaseUser' + Color.END + \
f': {self.user_id}, {self.username}, {self.display_name}, {self.uri}'

14
music/model/config.py Normal file
View File

@ -0,0 +1,14 @@
from fireo.models import Model
from fireo.fields import TextField, BooleanField, DateTime, NumberField, ListField
class Config(Model):
class Meta:
collection_name = 'config'
spotify_client_id = TextField()
spotify_client_secret = TextField()
last_fm_client_id = TextField()
playlist_cloud_operating_mode = TextField() # task, function
secret_key = TextField()

View File

@ -1,28 +1,36 @@
from flask import Flask, render_template, redirect, session, flash, url_for from flask import Flask, render_template, redirect, session, flash, url_for
from google.cloud import firestore
import logging
import os import os
from music.auth import auth_blueprint from music.auth import auth_blueprint
from music.api import api_blueprint, player_blueprint, fm_blueprint, \ from music.api import api_blueprint, player_blueprint, fm_blueprint, \
spotfm_blueprint, spotify_blueprint, admin_blueprint, tag_blueprint spotfm_blueprint, spotify_blueprint, admin_blueprint, tag_blueprint
from music.model.config import Config
db = firestore.Client() logger = logging.getLogger(__name__)
app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), '..', 'build'), template_folder="templates")
app.secret_key = db.collection(u'spotify').document(u'config').get().to_dict()['secret_key']
app.register_blueprint(auth_blueprint, url_prefix='/auth')
app.register_blueprint(api_blueprint, url_prefix='/api')
app.register_blueprint(player_blueprint, url_prefix='/api/player')
app.register_blueprint(fm_blueprint, url_prefix='/api/fm')
app.register_blueprint(spotfm_blueprint, url_prefix='/api/spotfm')
app.register_blueprint(spotify_blueprint, url_prefix='/api/spotify')
app.register_blueprint(admin_blueprint, url_prefix='/api/admin')
app.register_blueprint(tag_blueprint, url_prefix='/api')
@app.route('/') def create_app():
def index(): app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), '..', 'build'), template_folder="templates")
config = Config.collection.get("config/music-tools")
if config is not None:
app.secret_key = config.secret_key
else:
logger.error('no config returned, skipping secret key')
app.register_blueprint(auth_blueprint, url_prefix='/auth')
app.register_blueprint(api_blueprint, url_prefix='/api')
app.register_blueprint(player_blueprint, url_prefix='/api/player')
app.register_blueprint(fm_blueprint, url_prefix='/api/fm')
app.register_blueprint(spotfm_blueprint, url_prefix='/api/spotfm')
app.register_blueprint(spotify_blueprint, url_prefix='/api/spotify')
app.register_blueprint(admin_blueprint, url_prefix='/api/admin')
app.register_blueprint(tag_blueprint, url_prefix='/api')
@app.route('/')
def index():
if 'username' in session: if 'username' in session:
logged_in = True logged_in = True
@ -32,10 +40,9 @@ def index():
return render_template('login.html', logged_in=logged_in) return render_template('login.html', logged_in=logged_in)
@app.route('/app', defaults={'path': ''})
@app.route('/app', defaults={'path': ''}) @app.route('/app/<path:path>')
@app.route('/app/<path:path>') def app_route(path):
def app_route(path):
if 'username' not in session: if 'username' not in session:
flash('please log in') flash('please log in')
@ -43,4 +50,6 @@ def app_route(path):
return render_template('app.html') return render_template('app.html')
return app
# [END gae_python37_app] # [END gae_python37_app]

View File

@ -1,12 +1,8 @@
from google.cloud import firestore
import logging import logging
import music.db.database as database import music.db.database as database
from spotframework.net.network import SpotifyNetworkException from spotframework.net.network import SpotifyNetworkException
db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,5 +1,3 @@
from google.cloud import firestore
import logging import logging
from datetime import datetime from datetime import datetime
@ -13,8 +11,6 @@ from spotframework.net.network import SpotifyNetworkException
from fmframework.net.network import LastFMNetworkException from fmframework.net.network import LastFMNetworkException
db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,14 +0,0 @@
from music.model.user import User
from music.model.playlist import Playlist
user = User.collection.filter('username', '==', 'andy').get()
name = input('enter playlist name: ')
playlist = Playlist.collection.parent(user.key).filter('name', '==', name).get()
if playlist is not None:
new_name = input('enter new name: ')
playlist.name = new_name
playlist.update()
else:
print('playlist not found')

View File

@ -15,15 +15,17 @@ google-cloud-tasks==1.5.0
googleapis-common-protos==1.52.0 googleapis-common-protos==1.52.0
grpc-google-iam-v1==0.12.3 grpc-google-iam-v1==0.12.3
grpcio==1.30.0 grpcio==1.30.0
gunicorn==20.0.4
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.2 Jinja2==2.11.2
lazy-object-proxy==1.5.0 lazy-object-proxy==1.4.3
MarkupSafe==1.1.1 MarkupSafe==1.1.1
mccabe==0.6.1 mccabe==0.6.1
numpy==1.19.0 numpy==1.19.0
opencv-python==4.2.0.34 opencv-python==4.2.0.34
pathtools==0.1.2
protobuf==3.12.2 protobuf==3.12.2
pyasn1==0.4.8 pyasn1==0.4.8
pyasn1-modules==0.2.8 pyasn1-modules==0.2.8
@ -35,5 +37,6 @@ six==1.15.0
tabulate==0.8.7 tabulate==0.8.7
toml==0.10.1 toml==0.10.1
urllib3==1.25.9 urllib3==1.25.9
watchdog==0.10.3
Werkzeug==1.0.1 Werkzeug==1.0.1
wrapt==1.12.1 wrapt==1.12.1

1
sass
View File

@ -1 +0,0 @@
sass --style=compressed src/scss/style.scss build/style.css

View File

@ -1 +0,0 @@
sass --style=compressed --watch src/scss/style.scss build/style.css