From 29bd875ecdef777096c98f6b908c800ec63d2faa Mon Sep 17 00:00:00 2001 From: aj Date: Mon, 3 Feb 2020 23:37:18 +0000 Subject: [PATCH] added tag js, added playlist name sorting, moved cloud tasks --- deploy | 36 +++- music/api/api.py | 138 +------------ music/api/spotfm.py | 138 +------------ music/api/tag.py | 42 ++-- music/cloud/__init__.py | 0 music/cloud/function.py | 10 + music/cloud/tasks.py | 254 +++++++++++++++++++++++ music/db/database.py | 4 +- music/model/tag.py | 1 + src/js/Maths/BarChart.js | 20 +- src/js/MusicTools.js | 10 +- src/js/Playlist/View/Edit.js | 14 +- src/js/Tag/New.js | 85 ++++++++ src/js/Tag/TagList.js | 66 +----- src/js/Tag/TagRouter.js | 20 ++ src/js/Tag/View.js | 389 +++++++++++++++++++++++++++++++++++ 16 files changed, 867 insertions(+), 360 deletions(-) create mode 100644 music/cloud/__init__.py create mode 100644 music/cloud/function.py create mode 100644 music/cloud/tasks.py create mode 100644 src/js/Tag/New.js create mode 100644 src/js/Tag/TagRouter.js create mode 100644 src/js/Tag/View.js diff --git a/deploy b/deploy index ad17bf2..22b0468 100755 --- a/deploy +++ b/deploy @@ -22,14 +22,34 @@ cp -r spotfm/spotfm $stage_dir/ cd $stage_dir -echo '>> building css' -sass --style=compressed src/scss/style.scss build/style.css - -echo '>> building javascript' -npm run build - -echo '>> deploying' gcloud config set project sarsooxyz -gcloud app deploy + +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 + + diff --git a/music/api/api.py b/music/api/api.py index a69875e..a21132c 100644 --- a/music/api/api.py +++ b/music/api/api.py @@ -1,15 +1,14 @@ -from flask import Blueprint, session, request, jsonify +from flask import Blueprint, request, jsonify import os -import datetime import json import logging from google.cloud import firestore -from google.cloud import tasks_v2 -from google.protobuf import timestamp_pb2 from music.api.decorators import login_required, login_or_basic_auth, admin_required, gae_cron, cloud_task +from music.cloud.tasks import execute_all_user_playlists, execute_user_playlists, create_run_user_playlist_task, \ + create_play_user_playlist_task from music.tasks.run_user_playlist import run_user_playlist as run_user_playlist from music.tasks.play_user_playlist import play_user_playlist as play_user_playlist @@ -18,9 +17,6 @@ import music.db.database as database blueprint = Blueprint('api', __name__) db = firestore.Client() -tasker = tasks_v2.CloudTasksClient() -task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') - logger = logging.getLogger(__name__) @@ -411,12 +407,12 @@ def run_playlist_task(): def run_user(username=None): db_user = database.get_user(username) - if db_user.type == db_user.Type.admin: + if db_user.user_type == db_user.Type.admin: user_name = request.args.get('username', username) else: user_name = username - execute_user(user_name) + execute_user_playlists(user_name) return jsonify({'message': 'executed user', 'status': 'success'}), 200 @@ -427,7 +423,7 @@ def run_user_task(): payload = request.get_data(as_text=True) if payload: - execute_user(payload) + execute_user_playlists(payload) return jsonify({'message': 'executed user', 'status': 'success'}), 200 @@ -436,7 +432,7 @@ def run_user_task(): @admin_required def run_users(username=None): - execute_all_users() + execute_all_user_playlists() return jsonify({'message': 'executed all users', 'status': 'success'}), 200 @@ -444,123 +440,5 @@ def run_users(username=None): @gae_cron def run_users_cron(): - execute_all_users() + execute_all_user_playlists() return jsonify({'status': 'success'}), 200 - - -def execute_all_users(): - - seconds_delay = 0 - logger.info('running') - - for iter_user in database.get_users(): - - if iter_user.spotify_linked and not iter_user.locked: - - task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/playlist/run/user/task', - 'body': iter_user.username.encode() - } - } - - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds_delay) - - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - task['schedule_time'] = timestamp - - tasker.create_task(task_path, task) - seconds_delay += 30 - - -def execute_user(username): - - playlists = database.get_user_playlists(username) - - seconds_delay = 0 - logger.info(f'running {username}') - - for iterate_playlist in playlists: - if iterate_playlist.uri: - - if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': - create_run_user_playlist_task(username, iterate_playlist.name, seconds_delay) - else: - run_playlist(username, iterate_playlist.name) - - seconds_delay += 6 - - -def create_run_user_playlist_task(username, playlist_name, delay=0): - - task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/playlist/run/task', - 'body': json.dumps({ - 'username': username, - 'name': playlist_name - }).encode() - } - } - - if delay > 0: - - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) - - # Create Timestamp protobuf. - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - # Add the timestamp to the tasks. - task['schedule_time'] = timestamp - - tasker.create_task(task_path, task) - - -def create_play_user_playlist_task(username, - parts=None, - playlist_type='default', - playlists=None, - shuffle=False, - include_recommendations=False, - recommendation_sample=10, - day_boundary=10, - add_this_month=False, - add_last_month=False, - delay=0, - device_name=None): - task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/playlist/play/task', - 'body': json.dumps({ - 'username': username, - 'playlist_type': playlist_type, - 'parts': parts, - 'playlists': playlists, - 'shuffle': shuffle, - 'include_recommendations': include_recommendations, - 'recommendation_sample': recommendation_sample, - 'day_boundary': day_boundary, - 'add_this_month': add_this_month, - 'add_last_month': add_last_month, - 'device_name': device_name - }).encode() - } - } - - if delay > 0: - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) - - # Create Timestamp protobuf. - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - # Add the timestamp to the tasks. - task['schedule_time'] = timestamp - - tasker.create_task(task_path, task) diff --git a/music/api/spotfm.py b/music/api/spotfm.py index 3a90e57..4a9e04f 100644 --- a/music/api/spotfm.py +++ b/music/api/spotfm.py @@ -2,10 +2,10 @@ from flask import Blueprint, jsonify, request import logging import json import os -import datetime from music.api.decorators import admin_required, login_or_basic_auth, lastfm_username_required, spotify_link_required, cloud_task, gae_cron import music.db.database as database +from music.cloud.tasks import execute_all_user_playlist_stats, execute_user_playlist_stats, create_refresh_playlist_task from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats, \ refresh_lastfm_album_stats, \ refresh_lastfm_artist_stats @@ -13,17 +13,9 @@ from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats, \ from spotfm.maths.counter import Counter from spotframework.model.uri import Uri -from google.cloud import firestore -from google.cloud import tasks_v2 -from google.protobuf import timestamp_pb2 - blueprint = Blueprint('spotfm-api', __name__) logger = logging.getLogger(__name__) -db = firestore.Client() -tasker = tasks_v2.CloudTasksClient() -task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') - @blueprint.route('/count', methods=['GET']) @login_or_basic_auth @@ -144,14 +136,14 @@ def run_playlist_artist_task(): @login_or_basic_auth @admin_required def run_users(username=None): - execute_all_users() + execute_all_user_playlist_stats() return jsonify({'message': 'executed all users', 'status': 'success'}), 200 @blueprint.route('/playlist/refresh/users/cron', methods=['GET']) @gae_cron def run_users_task(): - execute_all_users() + execute_all_user_playlist_stats() return jsonify({'status': 'success'}), 200 @@ -165,7 +157,7 @@ def run_user(username=None): else: user_name = username - execute_user(user_name) + execute_user_playlist_stats(user_name) return jsonify({'message': 'executed user', 'status': 'success'}), 200 @@ -176,125 +168,5 @@ def run_user_task(): payload = request.get_data(as_text=True) if payload: - execute_user(payload) + execute_user_playlist_stats(payload) return jsonify({'message': 'executed user', 'status': 'success'}), 200 - - -def execute_all_users(): - - seconds_delay = 0 - logger.info('running') - - for iter_user in database.get_users(): - - if iter_user.spotify_linked and iter_user.lastfm_username and \ - len(iter_user.lastfm_username) > 0 and not iter_user.locked: - - if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': - create_refresh_user_task(username=iter_user.username, delay=seconds_delay) - else: - execute_user(username=iter_user.username) - - seconds_delay += 2400 - - else: - logger.debug(f'skipping {iter_user.username}') - - -def execute_user(username): - - playlists = database.get_user_playlists(username) - user = database.get_user(username) - - seconds_delay = 0 - logger.info(f'running {username}') - - if user.lastfm_username and len(user.lastfm_username) > 0: - for playlist in playlists: - if playlist.uri: - - if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': - create_refresh_playlist_task(username, playlist.name, seconds_delay) - else: - refresh_lastfm_track_stats(username, playlist.name) - - seconds_delay += 1200 - else: - logger.error('no last.fm username') - - -def create_refresh_user_task(username, delay=0): - - task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/spotfm/playlist/refresh/user/task', - 'body': username.encode() - } - } - - if delay > 0: - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) - - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - task['schedule_time'] = timestamp - - tasker.create_task(task_path, task) - - -def create_refresh_playlist_task(username, playlist_name, delay=0): - - track_task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/spotfm/playlist/refresh/task/track', - 'body': json.dumps({ - 'username': username, - 'name': playlist_name - }).encode() - } - } - if delay > 0: - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - track_task['schedule_time'] = timestamp - - album_task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/spotfm/playlist/refresh/task/album', - 'body': json.dumps({ - 'username': username, - 'name': playlist_name - }).encode() - } - } - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay + 180) - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - album_task['schedule_time'] = timestamp - - artist_task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': 'POST', - 'relative_uri': '/api/spotfm/playlist/refresh/task/artist', - 'body': json.dumps({ - 'username': username, - 'name': playlist_name - }).encode() - } - } - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay + 360) - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - artist_task['schedule_time'] = timestamp - - tasker.create_task(task_path, track_task) - tasker.create_task(task_path, album_task) - tasker.create_task(task_path, artist_task) diff --git a/music/api/tag.py b/music/api/tag.py index c2fe469..e690834 100644 --- a/music/api/tag.py +++ b/music/api/tag.py @@ -2,16 +2,13 @@ from flask import Blueprint, jsonify, request import logging -from google.cloud import pubsub_v1 - import music.db.database as database from music.api.decorators import login_or_basic_auth +from music.cloud.function import update_tag blueprint = Blueprint('task', __name__) logger = logging.getLogger(__name__) -publisher = pubsub_v1.PublisherClient() - @blueprint.route('/tag', methods=['GET']) @login_or_basic_auth @@ -26,7 +23,7 @@ def tags(username=None): @login_or_basic_auth def tag(tag_id, username=None): if request.method == 'GET': - return put_tag(tag_id, username) + return get_tag(tag_id, username) elif request.method == 'PUT': return put_tag(tag_id, username) elif request.method == 'POST': @@ -63,7 +60,7 @@ def put_tag(tag_id, username): update_required = False tracks = [] - if request_json.get('tracks'): + if request_json.get('tracks') is not None: update_required = True for track in request_json['tracks']: if track.get('name') and track.get('artist'): @@ -71,10 +68,10 @@ def put_tag(tag_id, username): 'name': track['name'], 'artist': track['artist'] }) - db_tag.tracks = tracks + db_tag.tracks = tracks albums = [] - if request_json.get('albums'): + if request_json.get('albums') is not None: update_required = True for album in request_json['albums']: if album.get('name') and album.get('artist'): @@ -82,17 +79,17 @@ def put_tag(tag_id, username): 'name': album['name'], 'artist': album['artist'] }) - db_tag.album = albums + db_tag.albums = albums artists = [] - if request_json.get('artists'): + if request_json.get('artists') is not None: update_required = True - for artist in request_json['tracks']: - if artist.get('name') and artist.get('artist'): + for artist in request_json['artists']: + if artist.get('name'): artists.append({ 'name': artist['name'] }) - db_tag.artists = artists + db_tag.artists = artists if update_required: update_tag(username=username, tag_id=tag_id) @@ -103,11 +100,10 @@ def put_tag(tag_id, username): def post_tag(tag_id, username): logger.info(f'creating {tag_id} for {username}') - new_tag = database.create_tag(username=username, tag_id=tag_id) - if new_tag is not None: - return jsonify({"message": 'tag added', "status": "success"}), 201 - else: - return jsonify({"error": 'tag not created'}), 400 + tag_id = tag_id.replace(' ', '_') + + database.create_tag(username=username, tag_id=tag_id) + return jsonify({"message": 'tag added', "status": "success"}), 201 def delete_tag(tag_id, username): @@ -121,7 +117,9 @@ def delete_tag(tag_id, username): return jsonify({"error": 'tag not deleted'}), 400 -def update_tag(username, tag_id): - logger.info(f'queuing {tag_id} update for {username}') - - publisher.publish('projects/sarsooxyz/topics/update_tag', b'', tag_id=tag_id, username=username) +@blueprint.route('/tag//update', methods=['GET']) +@login_or_basic_auth +def tag_refresh(tag_id, username=None): + logger.info(f'updating {tag_id} tag for {username}') + update_tag(username=username, tag_id=tag_id) + return jsonify({"message": 'tag updated', "status": "success"}), 200 diff --git a/music/cloud/__init__.py b/music/cloud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/music/cloud/function.py b/music/cloud/function.py new file mode 100644 index 0000000..5d48310 --- /dev/null +++ b/music/cloud/function.py @@ -0,0 +1,10 @@ +import logging +from google.cloud import pubsub_v1 + +publisher = pubsub_v1.PublisherClient() +logger = logging.getLogger(__name__) + + +def update_tag(username, tag_id): + logger.info(f'queuing {tag_id} update for {username}') + publisher.publish('projects/sarsooxyz/topics/update_tag', b'', tag_id=tag_id, username=username) diff --git a/music/cloud/tasks.py b/music/cloud/tasks.py new file mode 100644 index 0000000..c876096 --- /dev/null +++ b/music/cloud/tasks.py @@ -0,0 +1,254 @@ +import datetime +import json +import os +import logging + +from google.cloud import tasks_v2 +from google.protobuf import timestamp_pb2 + +from music.db import database as database +from music.tasks.run_user_playlist import run_user_playlist +from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats + +tasker = tasks_v2.CloudTasksClient() +task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') + +logger = logging.getLogger(__name__) + + +def execute_all_user_playlists(): + + seconds_delay = 0 + logger.info('running') + + for iter_user in database.get_users(): + + if iter_user.spotify_linked and not iter_user.locked: + + task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/playlist/run/user/task', + 'body': iter_user.username.encode() + } + } + + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds_delay) + + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + task['schedule_time'] = timestamp + + tasker.create_task(task_path, task) + seconds_delay += 30 + + +def execute_user_playlists(username): + + playlists = database.get_user_playlists(username) + + seconds_delay = 0 + logger.info(f'running {username}') + + for iterate_playlist in playlists: + if iterate_playlist.uri: + + if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': + create_run_user_playlist_task(username, iterate_playlist.name, seconds_delay) + else: + run_user_playlist(username, iterate_playlist.name) + + seconds_delay += 6 + + +def create_run_user_playlist_task(username, playlist_name, delay=0): + + task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/playlist/run/task', + 'body': json.dumps({ + 'username': username, + 'name': playlist_name + }).encode() + } + } + + if delay > 0: + + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) + + # Create Timestamp protobuf. + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + # Add the timestamp to the tasks. + task['schedule_time'] = timestamp + + tasker.create_task(task_path, task) + + +def create_play_user_playlist_task(username, + parts=None, + playlist_type='default', + playlists=None, + shuffle=False, + include_recommendations=False, + recommendation_sample=10, + day_boundary=10, + add_this_month=False, + add_last_month=False, + delay=0, + device_name=None): + task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/playlist/play/task', + 'body': json.dumps({ + 'username': username, + 'playlist_type': playlist_type, + 'parts': parts, + 'playlists': playlists, + 'shuffle': shuffle, + 'include_recommendations': include_recommendations, + 'recommendation_sample': recommendation_sample, + 'day_boundary': day_boundary, + 'add_this_month': add_this_month, + 'add_last_month': add_last_month, + 'device_name': device_name + }).encode() + } + } + + if delay > 0: + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) + + # Create Timestamp protobuf. + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + # Add the timestamp to the tasks. + task['schedule_time'] = timestamp + + tasker.create_task(task_path, task) + + +def execute_all_user_playlist_stats(): + + seconds_delay = 0 + logger.info('running') + + for iter_user in database.get_users(): + + if iter_user.spotify_linked and iter_user.lastfm_username and \ + len(iter_user.lastfm_username) > 0 and not iter_user.locked: + + if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': + create_refresh_user_task(username=iter_user.username, delay=seconds_delay) + else: + execute_user_playlist_stats(username=iter_user.username) + + seconds_delay += 2400 + + else: + logger.debug(f'skipping {iter_user.username}') + + +def execute_user_playlist_stats(username): + + playlists = database.get_user_playlists(username) + user = database.get_user(username) + + seconds_delay = 0 + logger.info(f'running {username}') + + if user.lastfm_username and len(user.lastfm_username) > 0: + for playlist in playlists: + if playlist.uri: + + if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': + create_refresh_playlist_task(username, playlist.name, seconds_delay) + else: + refresh_lastfm_track_stats(username, playlist.name) + + seconds_delay += 1200 + else: + logger.error('no last.fm username') + + +def create_refresh_user_task(username, delay=0): + + task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/spotfm/playlist/refresh/user/task', + 'body': username.encode() + } + } + + if delay > 0: + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) + + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + task['schedule_time'] = timestamp + + tasker.create_task(task_path, task) + + +def create_refresh_playlist_task(username, playlist_name, delay=0): + + track_task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/spotfm/playlist/refresh/task/track', + 'body': json.dumps({ + 'username': username, + 'name': playlist_name + }).encode() + } + } + if delay > 0: + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + track_task['schedule_time'] = timestamp + + album_task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/spotfm/playlist/refresh/task/album', + 'body': json.dumps({ + 'username': username, + 'name': playlist_name + }).encode() + } + } + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay + 180) + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + album_task['schedule_time'] = timestamp + + artist_task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/spotfm/playlist/refresh/task/artist', + 'body': json.dumps({ + 'username': username, + 'name': playlist_name + }).encode() + } + } + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay + 360) + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + artist_task['schedule_time'] = timestamp + + tasker.create_task(task_path, track_task) + tasker.create_task(task_path, album_task) + tasker.create_task(task_path, artist_task) diff --git a/music/db/database.py b/music/db/database.py index a1209cb..d93f116 100644 --- a/music/db/database.py +++ b/music/db/database.py @@ -435,7 +435,7 @@ def create_tag(username: str, tag_id: str): logger.error(f'{tag_id} already exists for {username}') return None - return parse_tag_reference(user.db_ref.collection(u'tags').add({ + user.db_ref.collection(u'tags').add({ 'tag_id': tag_id, 'name': tag_id, @@ -447,4 +447,4 @@ def create_tag(username: str, tag_id: str): 'proportion': 0.0, 'total_user_scrobbles': 0, 'last_updated': None - })[1]) + }) diff --git a/music/model/tag.py b/music/model/tag.py index 2f24211..88f55f4 100644 --- a/music/model/tag.py +++ b/music/model/tag.py @@ -48,6 +48,7 @@ class Tag: 'count': self.count, 'proportion': self.proportion, + 'total_user_scrobbles': self.total_user_scrobbles, 'last_updated': self.last_updated } diff --git a/src/js/Maths/BarChart.js b/src/js/Maths/BarChart.js index 98d2ad7..317abad 100644 --- a/src/js/Maths/BarChart.js +++ b/src/js/Maths/BarChart.js @@ -29,13 +29,21 @@ class BarChart extends Component { } }, scales: { - yAxes: [ - { - ticks: { - min: 0 - } + yAxes: [{ + ticks: { + fontColor: "#d8d8d8", + fontSize: 16, + stepSize: 1, + beginAtZero: true } - ] + }], + xAxes: [{ + ticks: { + fontColor: "#d8d8d8", + fontSize: 16, + stepSize: 1 + } + }] } } }); diff --git a/src/js/MusicTools.js b/src/js/MusicTools.js index 3f2bc6c..32b320d 100644 --- a/src/js/MusicTools.js +++ b/src/js/MusicTools.js @@ -22,7 +22,7 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import HomeIcon from '@material-ui/icons/Home'; -import { Build, PieChart, QueueMusic, ExitToApp, AccountCircle, KeyboardBackspace } from '@material-ui/icons' +import { Build, PieChart, QueueMusic, ExitToApp, AccountCircle, KeyboardBackspace, GroupWork } from '@material-ui/icons' const axios = require('axios'); @@ -32,6 +32,8 @@ const LazyPlaylists = React.lazy(() => import("./Playlist/AllPlaylistsRouter")) const LazyPlaylistView = React.lazy(() => import("./Playlist/View/PlaylistRouter")) const LazySettings = React.lazy(() => import("./Settings/SettingsRouter")) const LazyAdmin = React.lazy(() => import("./Admin/AdminRouter")) +const LazyTags = React.lazy(() => import("./Tag/TagRouter")) +const LazyTag = React.lazy(() => import("./Tag/View")) class MusicTools extends Component { @@ -117,6 +119,10 @@ class MusicTools extends Component { + + + + @@ -147,6 +153,8 @@ class MusicTools extends Component { }> + + { this.state.type == 'admin' && } diff --git a/src/js/Playlist/View/Edit.js b/src/js/Playlist/View/Edit.js index 4745855..54f2066 100644 --- a/src/js/Playlist/View/Edit.js +++ b/src/js/Playlist/View/Edit.js @@ -97,18 +97,24 @@ export class Edit extends Component{ axios.all([this.getPlaylistInfo(), this.getPlaylists()]) .then(axios.spread((info, playlists) => { - info.data.parts.sort(function(a, b){ + info.data.parts.sort((a, b) => { if(a.toLowerCase() < b.toLowerCase()) { return -1; } if(a.toLowerCase() > b.toLowerCase()) { return 1; } return 0; }); - info.data.playlist_references.sort(function(a, b){ + info.data.playlist_references.sort((a, b) => { if(a.toLowerCase() < b.toLowerCase()) { return -1; } if(a.toLowerCase() > b.toLowerCase()) { return 1; } return 0; }); + playlists.data.playlists.sort( (a, b) => { + if(a.name.toLowerCase() < b.name.toLowerCase()) { return -1; } + if(a.name.toLowerCase() > b.name.toLowerCase()) { return 1; } + return 0; + }); + var filteredPlaylists = playlists.data.playlists.filter((entry) => entry.name != this.state.name); this.setState(info.data); @@ -421,7 +427,7 @@ export class Edit extends Component{ @@ -485,6 +492,7 @@ export class Edit extends Component{ diff --git a/src/js/Tag/New.js b/src/js/Tag/New.js new file mode 100644 index 0000000..168f25f --- /dev/null +++ b/src/js/Tag/New.js @@ -0,0 +1,85 @@ +import React, { Component } from "react"; +const axios = require('axios'); + +import { Card, Button, TextField, CardActions, CardContent, Typography, Grid } from '@material-ui/core'; + +import showMessage from "../Toast.js" + +class NewTag extends Component { + + constructor(props) { + super(props); + this.state = { + tag_id: '' + } + this.handleInputChange = this.handleInputChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleInputChange(event){ + this.setState({ + tag_id: event.target.value + }); + } + + handleSubmit(event){ + var tag_id = this.state.tag_id; + this.setState({ + tag_id: '' + }); + + if(tag_id.length != 0){ + axios.get('/api/tag') + .then((response) => { + var tag_ids = response.data.tags.map(entry => entry.tag_id) + + var sameTag_id = tag_ids.includes(this.state.tag_id); + if(sameTag_id == false){ + axios.post(`/api/tag/${tag_id}`).then((response) => { + showMessage(`${tag_id} Created`); + }).catch((error) => { + showMessage(`Error Creating Tag (${error.response.status})`); + }); + }else{ + showMessage('Named Tag Already Exists'); + } + }) + .catch((error) => { + showMessage(`Error Getting Tags (${error.response.status})`); + }); + }else{ + showMessage('Enter Name'); + } + } + + render(){ + return ( +
+ + + + + New Tag + + + + + + + + + + +
+ ); + } + +} + +export default NewTag; \ No newline at end of file diff --git a/src/js/Tag/TagList.js b/src/js/Tag/TagList.js index 3237fc1..999f51d 100644 --- a/src/js/Tag/TagList.js +++ b/src/js/Tag/TagList.js @@ -15,14 +15,12 @@ class TagList extends Component { isLoading: true } this.getTags(); - this.handleRunTag = this.handleRunTag.bind(this); this.handleDeleteTag = this.handleDeleteTag.bind(this); - this.handleRunAll = this.handleRunAll.bind(this); } getTags(){ var self = this; - axios.get('/api/tags') + axios.get('/api/tag') .then((response) => { var tags = response.data.tags.slice(); @@ -34,69 +32,29 @@ class TagList extends Component { }); self.setState({ - playlists: tags, + tags: tags, isLoading: false }); }) .catch((error) => { - showMessage(`Error Getting Playlists (${error.response.status})`); + showMessage(`Error Getting Tags (${error.response.status})`); }); } - handleRunTag(name, event){ - axios.get('/api/user') + handleDeleteTag(tag_id, event){ + axios.delete(`/api/tag/${tag_id}`) .then((response) => { - if(response.data.spotify_linked == true){ - axios.get('/api/tag/run', {params: {name: name}}) - .then((response) => { - showMessage(`${name} ran`); - }) - .catch((error) => { - showMessage(`Error Running ${name} (${error.response.status})`); - }); - }else{ - showMessage(`Link Spotify Before Running`); - } - }).catch((error) => { - showMessage(`Error Running ${this.state.name} (${error.response.status})`); - }); - } - - handleDeleteTag(name, event){ - axios.delete('/api/playlist', { params: { name: name } }) - .then((response) => { - showMessage(`${name} Deleted`); + showMessage(`${tag_id} Deleted`); this.getTags(); }).catch((error) => { - showMessage(`Error Deleting ${name} (${error.response.status})`); - }); - } - - handleRunAll(event){ - axios.get('/api/user') - .then((response) => { - if(response.data.spotify_linked == true){ - axios.get('/api/tag/run/user') - .then((response) => { - showMessage("All Tags Ran"); - }) - .catch((error) => { - showMessage(`Error Running All (${error.response.status})`); - }); - }else{ - showMessage(`Link Spotify Before Running`); - } - }).catch((error) => { - showMessage(`Error Running ${this.state.name} (${error.response.status})`); + showMessage(`Error Deleting ${tag_id} (${error.response.status})`); }); } render() { const grid = ; + handleDeleteTag={this.handleDeleteTag}/>; return this.state.isLoading ? : grid; } @@ -116,7 +74,6 @@ function TagGrid(props){ orientation="vertical" className="full-width"> - { props.tags.length == 0 ? ( @@ -146,9 +103,8 @@ function TagCard(props){ - - - + + @@ -157,7 +113,7 @@ function TagCard(props){ } function getTagLink(tagName){ - return `/app/tag/${tagName}/edit`; + return `/app/tag/${tagName}`; } export default TagList; \ No newline at end of file diff --git a/src/js/Tag/TagRouter.js b/src/js/Tag/TagRouter.js new file mode 100644 index 0000000..346613c --- /dev/null +++ b/src/js/Tag/TagRouter.js @@ -0,0 +1,20 @@ +import React, { Component } from "react"; +import { Route, Switch } from "react-router-dom"; + +import TagList from "./TagList.js" +import New from "./New.js" + +class TagRouter extends Component { + render(){ + return ( +
+ + + + +
+ ); + } +} + +export default TagRouter; \ No newline at end of file diff --git a/src/js/Tag/View.js b/src/js/Tag/View.js new file mode 100644 index 0000000..f73c023 --- /dev/null +++ b/src/js/Tag/View.js @@ -0,0 +1,389 @@ +import React, { Component } from "react"; +const axios = require('axios'); + +import { Card, Button, CircularProgress, CardActions, CardContent, FormControl, InputLabel, Select, Typography, Grid, TextField } from '@material-ui/core'; +import { Delete } from '@material-ui/icons'; +import { makeStyles } from '@material-ui/core/styles'; + +import showMessage from "../Toast.js"; +import BarChart from "../Maths/BarChart.js"; +import PieChart from "../Maths/PieChart.js"; + +const useStyles = makeStyles({ + root: { + background: '#9e9e9e', + color: '#212121', + align: "center" + }, + }); + +class View extends Component{ + + constructor(props){ + super(props); + this.state = { + tag_id: props.match.params.tag_id, + tag: { + name: "", + tracks: [], + albums: [], + artists: [] + }, + + addType: 'artists', + + name: '', + artist: '', + + isLoading: true + } + this.handleInputChange = this.handleInputChange.bind(this); + this.handleRun = this.handleRun.bind(this); + this.handleRemoveObj = this.handleRemoveObj.bind(this); + + this.handleAdd = this.handleAdd.bind(this); + this.handleChangeAddType = this.handleChangeAddType.bind(this); + } + + componentDidMount(){ + this.getTag(); + var intervalId = setInterval(() => {this.getTag(false)}, 5000); + var timeoutId = setTimeout(() => {clearInterval(this.state.intervalId)}, 300000); + + this.setState({ + intervalId: intervalId, + timeoutId: timeoutId + }); + } + + componentWillUnmount(){ + clearInterval(this.state.intervalId); + clearTimeout(this.state.timeoutId); + } + + getTag(error_toast = true){ + axios.get(`/api/tag/${ this.state.tag_id }`) + .then( (response) => { + + var tag = response.data.tag; + + tag.artists = tag.artists.sort((a, b) => { + if(a.name.toLowerCase() < b.name.toLowerCase()) { return -1; } + if(a.name.toLowerCase() > b.name.toLowerCase()) { return 1; } + return 0; + }); + + tag.albums = tag.albums.sort((a, b) => { + if(a.artist.toLowerCase() < b.artist.toLowerCase()) { return -1; } + if(a.artist.toLowerCase() > b.artist.toLowerCase()) { return 1; } + return 0; + }); + + tag.tracks = tag.tracks.sort((a, b) => { + if(a.artist.toLowerCase() < b.artist.toLowerCase()) { return -1; } + if(a.artist.toLowerCase() > b.artist.toLowerCase()) { return 1; } + return 0; + }); + + this.setState({ + tag: response.data.tag, + isLoading: false + }); + }) + .catch( (error) => { + if(error_toast) { + showMessage(`Error Getting Tag Info (${error.response.status})`); + } + }); + } + + handleInputChange(event){ + + this.setState({ + [event.target.name]: event.target.value + }); + + } + + handleRun(event){ + axios.get('/api/user') + .then((response) => { + if(response.data.lastfm_username != null){ + axios.get(`/api/tag/${this.state.tag_id}/update`) + .then((reponse) => { + showMessage(`${this.state.name} Updating`); + }) + .catch((error) => { + showMessage(`Error Updating ${this.state.tag_id} (${error.response.status})`); + }); + }else{ + showMessage(`Add a Last.fm Username In Settings`); + } + }).catch((error) => { + showMessage(`Error Updating ${this.state.tag_id} (${error.response.status})`); + }); + } + + handleRemoveObj(music_obj, addType, event){ + var startingItems = this.state.tag[addType].slice(); + + var items = this.state.tag[addType].slice(); + items = items.filter((item) => { + if(addType == 'albums' || addType == 'tracks') { + return item.name.toLowerCase() != music_obj.name.toLowerCase() || item.artist.toLowerCase() != music_obj.artist.toLowerCase(); + }else{ + return item.name.toLowerCase() != music_obj.name.toLowerCase(); + } + }); + + var tag = this.state.tag; + tag[addType] = items; + + this.setState({ + tag: tag + }); + + axios.put(`/api/tag/${this.state.tag_id}`, { + [addType]: items + }) + .catch( (error) => { + showMessage(`Error Removing ${music_obj.name} (${error.response.status})`); + + redo_tag[addType] = startingItems; + this.setState({ + tag: redo_tag + }); + }); + } + + handleChangeAddType(type){ + this.setState({ + addType: type + }) + } + + handleAdd(){ + + var addType = this.state.addType; + var music_obj = { + name: this.state.name, + artist: this.state.artist + } + + if(music_obj.name == ''){ + showMessage(`Enter Name`); + return; + } + + if(music_obj.artist == '' && addType != 'artists'){ + showMessage(`Enter Artist`); + return; + } + + var list = this.state.tag[addType].slice().filter((item) => { + if(addType != 'artists') { + return item.name.toLowerCase() == music_obj.name.toLowerCase() && item.artist.toLowerCase() == music_obj.artist.toLowerCase(); + }else{ + return item.name.toLowerCase() == music_obj.name.toLowerCase(); + } + }); + + if(list.length != 0) { + showMessage(`${music_obj.name} already present`); + return; + } + + list = this.state.tag[addType].slice(); + if(addType == 'artist'){ + list.push({ + name: music_obj.name + }); + }else{ + list.push(music_obj); + } + + if(addType == "artists"){ + list = list.sort((a, b) => { + if(a.name.toLowerCase() < b.name.toLowerCase()) { return -1; } + if(a.name.toLowerCase() > b.name.toLowerCase()) { return 1; } + return 0; + }); + }else{ + list = list.sort((a, b) => { + if(a.artist.toLowerCase() < b.artist.toLowerCase()) { return -1; } + if(a.artist.toLowerCase() > b.artist.toLowerCase()) { return 1; } + return 0; + }); + } + + var tag = this.state.tag; + tag[addType] = list; + + this.setState({ + tag: tag, + name: '', + artist: '' + }); + + axios.put(`/api/tag/${this.state.tag_id}`, { + [addType]: list + }) + .catch( (error) => { + showMessage(`Error Adding ${music_obj.name} (${error.response.status})`); + }); + } + + render(){ + + var all = [...this.state.tag.artists, ...this.state.tag.albums, ...this.state.tag.tracks]; + + var data = all.map((entry) => { + return { + "label": entry.name, + "value": entry.count + }; + }).sort((a, b) => { + if(a.value < b.value) { return 1; } + if(a.value > b.value) { return -1; } + return 0; + }); + + const table = ( +
+ + + {this.state.tag.name} + + + { this.state.tag.artists.length > 0 && Artists } + { this.state.tag.artists.length > 0 && } + + { this.state.tag.albums.length > 0 && Albums } + { this.state.tag.albums.length > 0 && } + + { this.state.tag.tracks.length > 0 && Tracks } + { this.state.tag.tracks.length > 0 && } + + + + { this.state.addType != 'artists' && + + + + } + + + Type + + + + + + + + + + + + + + + + + + + +
+ ); + + return this.state.isLoading ? : table; + } +} + +export default View; + +function ListBlock(props) { + return + {props.list.map((music_obj) => )} + +} + +function BlockGridItem (props) { + const classes = useStyles(); + return ( + + + + + + { props.music_obj.name } + + { 'artist' in props.music_obj && + + { props.music_obj.artist } + + } + { 'count' in props.music_obj && + + { props.music_obj.count } + + } + + + + + + + + ); +} + +function StatsCard (props) { + const classes = useStyles(); + return ( + + + + + + = { props.count } + + + { Math.round(props.proportion) }% + + + + + + ); +}