added tag js, added playlist name sorting, moved cloud tasks

This commit is contained in:
aj 2020-02-03 23:37:18 +00:00
parent 97ffc1f141
commit 29bd875ecd
16 changed files with 867 additions and 360 deletions

36
deploy
View File

@ -22,14 +22,34 @@ cp -r spotfm/spotfm $stage_dir/
cd $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 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

View File

@ -1,15 +1,14 @@
from flask import Blueprint, session, request, jsonify from flask import Blueprint, request, jsonify
import os import os
import datetime
import json import json
import logging import logging
from google.cloud import firestore 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.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.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
@ -18,9 +17,6 @@ import music.db.database as database
blueprint = Blueprint('api', __name__) blueprint = Blueprint('api', __name__)
db = firestore.Client() db = firestore.Client()
tasker = tasks_v2.CloudTasksClient()
task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -411,12 +407,12 @@ def run_playlist_task():
def run_user(username=None): def run_user(username=None):
db_user = database.get_user(username) 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) user_name = request.args.get('username', username)
else: else:
user_name = username user_name = username
execute_user(user_name) execute_user_playlists(user_name)
return jsonify({'message': 'executed user', 'status': 'success'}), 200 return jsonify({'message': 'executed user', 'status': 'success'}), 200
@ -427,7 +423,7 @@ def run_user_task():
payload = request.get_data(as_text=True) payload = request.get_data(as_text=True)
if payload: if payload:
execute_user(payload) execute_user_playlists(payload)
return jsonify({'message': 'executed user', 'status': 'success'}), 200 return jsonify({'message': 'executed user', 'status': 'success'}), 200
@ -436,7 +432,7 @@ def run_user_task():
@admin_required @admin_required
def run_users(username=None): def run_users(username=None):
execute_all_users() execute_all_user_playlists()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200 return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@ -444,123 +440,5 @@ def run_users(username=None):
@gae_cron @gae_cron
def run_users_cron(): def run_users_cron():
execute_all_users() execute_all_user_playlists()
return jsonify({'status': 'success'}), 200 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)

View File

@ -2,10 +2,10 @@ from flask import Blueprint, jsonify, request
import logging import logging
import json import json
import os 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 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 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, \ from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats, \
refresh_lastfm_album_stats, \ refresh_lastfm_album_stats, \
refresh_lastfm_artist_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 spotfm.maths.counter import Counter
from spotframework.model.uri import Uri 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__) blueprint = Blueprint('spotfm-api', __name__)
logger = logging.getLogger(__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']) @blueprint.route('/count', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@ -144,14 +136,14 @@ def run_playlist_artist_task():
@login_or_basic_auth @login_or_basic_auth
@admin_required @admin_required
def run_users(username=None): def run_users(username=None):
execute_all_users() execute_all_user_playlist_stats()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200 return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@blueprint.route('/playlist/refresh/users/cron', methods=['GET']) @blueprint.route('/playlist/refresh/users/cron', methods=['GET'])
@gae_cron @gae_cron
def run_users_task(): def run_users_task():
execute_all_users() execute_all_user_playlist_stats()
return jsonify({'status': 'success'}), 200 return jsonify({'status': 'success'}), 200
@ -165,7 +157,7 @@ def run_user(username=None):
else: else:
user_name = username user_name = username
execute_user(user_name) execute_user_playlist_stats(user_name)
return jsonify({'message': 'executed user', 'status': 'success'}), 200 return jsonify({'message': 'executed user', 'status': 'success'}), 200
@ -176,125 +168,5 @@ def run_user_task():
payload = request.get_data(as_text=True) payload = request.get_data(as_text=True)
if payload: if payload:
execute_user(payload) execute_user_playlist_stats(payload)
return jsonify({'message': 'executed user', 'status': 'success'}), 200 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)

View File

@ -2,16 +2,13 @@ from flask import Blueprint, jsonify, request
import logging import logging
from google.cloud import pubsub_v1
import music.db.database as database 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
blueprint = Blueprint('task', __name__) blueprint = Blueprint('task', __name__)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
publisher = pubsub_v1.PublisherClient()
@blueprint.route('/tag', methods=['GET']) @blueprint.route('/tag', methods=['GET'])
@login_or_basic_auth @login_or_basic_auth
@ -26,7 +23,7 @@ def tags(username=None):
@login_or_basic_auth @login_or_basic_auth
def tag(tag_id, username=None): def tag(tag_id, username=None):
if request.method == 'GET': if request.method == 'GET':
return put_tag(tag_id, username) return get_tag(tag_id, username)
elif request.method == 'PUT': elif request.method == 'PUT':
return put_tag(tag_id, username) return put_tag(tag_id, username)
elif request.method == 'POST': elif request.method == 'POST':
@ -63,7 +60,7 @@ def put_tag(tag_id, username):
update_required = False update_required = False
tracks = [] tracks = []
if request_json.get('tracks'): if request_json.get('tracks') is not None:
update_required = True update_required = True
for track in request_json['tracks']: for track in request_json['tracks']:
if track.get('name') and track.get('artist'): if track.get('name') and track.get('artist'):
@ -74,7 +71,7 @@ def put_tag(tag_id, username):
db_tag.tracks = tracks db_tag.tracks = tracks
albums = [] albums = []
if request_json.get('albums'): if request_json.get('albums') is not None:
update_required = True update_required = True
for album in request_json['albums']: for album in request_json['albums']:
if album.get('name') and album.get('artist'): if album.get('name') and album.get('artist'):
@ -82,13 +79,13 @@ def put_tag(tag_id, username):
'name': album['name'], 'name': album['name'],
'artist': album['artist'] 'artist': album['artist']
}) })
db_tag.album = albums db_tag.albums = albums
artists = [] artists = []
if request_json.get('artists'): if request_json.get('artists') is not None:
update_required = True update_required = True
for artist in request_json['tracks']: for artist in request_json['artists']:
if artist.get('name') and artist.get('artist'): if artist.get('name'):
artists.append({ artists.append({
'name': artist['name'] 'name': artist['name']
}) })
@ -103,11 +100,10 @@ def put_tag(tag_id, username):
def post_tag(tag_id, username): def post_tag(tag_id, username):
logger.info(f'creating {tag_id} for {username}') logger.info(f'creating {tag_id} for {username}')
new_tag = database.create_tag(username=username, tag_id=tag_id) tag_id = tag_id.replace(' ', '_')
if new_tag is not None:
database.create_tag(username=username, tag_id=tag_id)
return jsonify({"message": 'tag added', "status": "success"}), 201 return jsonify({"message": 'tag added', "status": "success"}), 201
else:
return jsonify({"error": 'tag not created'}), 400
def delete_tag(tag_id, username): def delete_tag(tag_id, username):
@ -121,7 +117,9 @@ def delete_tag(tag_id, username):
return jsonify({"error": 'tag not deleted'}), 400 return jsonify({"error": 'tag not deleted'}), 400
def update_tag(username, tag_id): @blueprint.route('/tag/<tag_id>/update', methods=['GET'])
logger.info(f'queuing {tag_id} update for {username}') @login_or_basic_auth
def tag_refresh(tag_id, username=None):
publisher.publish('projects/sarsooxyz/topics/update_tag', b'', tag_id=tag_id, username=username) 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

0
music/cloud/__init__.py Normal file
View File

10
music/cloud/function.py Normal file
View File

@ -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)

254
music/cloud/tasks.py Normal file
View File

@ -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)

View File

@ -435,7 +435,7 @@ def create_tag(username: str, tag_id: str):
logger.error(f'{tag_id} already exists for {username}') logger.error(f'{tag_id} already exists for {username}')
return None return None
return parse_tag_reference(user.db_ref.collection(u'tags').add({ user.db_ref.collection(u'tags').add({
'tag_id': tag_id, 'tag_id': tag_id,
'name': tag_id, 'name': tag_id,
@ -447,4 +447,4 @@ def create_tag(username: str, tag_id: str):
'proportion': 0.0, 'proportion': 0.0,
'total_user_scrobbles': 0, 'total_user_scrobbles': 0,
'last_updated': None 'last_updated': None
})[1]) })

View File

@ -48,6 +48,7 @@ class Tag:
'count': self.count, 'count': self.count,
'proportion': self.proportion, 'proportion': self.proportion,
'total_user_scrobbles': self.total_user_scrobbles,
'last_updated': self.last_updated 'last_updated': self.last_updated
} }

View File

@ -29,13 +29,21 @@ class BarChart extends Component {
} }
}, },
scales: { scales: {
yAxes: [ yAxes: [{
{
ticks: { ticks: {
min: 0 fontColor: "#d8d8d8",
fontSize: 16,
stepSize: 1,
beginAtZero: true
} }
}],
xAxes: [{
ticks: {
fontColor: "#d8d8d8",
fontSize: 16,
stepSize: 1
} }
] }]
} }
} }
}); });

View File

@ -22,7 +22,7 @@ import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import HomeIcon from '@material-ui/icons/Home'; 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'); const axios = require('axios');
@ -32,6 +32,8 @@ const LazyPlaylists = React.lazy(() => import("./Playlist/AllPlaylistsRouter"))
const LazyPlaylistView = React.lazy(() => import("./Playlist/View/PlaylistRouter")) const LazyPlaylistView = React.lazy(() => import("./Playlist/View/PlaylistRouter"))
const LazySettings = React.lazy(() => import("./Settings/SettingsRouter")) const LazySettings = React.lazy(() => import("./Settings/SettingsRouter"))
const LazyAdmin = React.lazy(() => import("./Admin/AdminRouter")) 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 { class MusicTools extends Component {
@ -117,6 +119,10 @@ class MusicTools extends Component {
<ListItemIcon><QueueMusic /></ListItemIcon> <ListItemIcon><QueueMusic /></ListItemIcon>
<ListItemText primary="Playlists" /> <ListItemText primary="Playlists" />
</ListItem> </ListItem>
<ListItem button key="tags" component={Link} to='/app/tags'>
<ListItemIcon><GroupWork /></ListItemIcon>
<ListItemText primary="Tags" />
</ListItem>
<ListItem button key="maths" component={Link} to='/app/maths/count'> <ListItem button key="maths" component={Link} to='/app/maths/count'>
<ListItemIcon><PieChart /></ListItemIcon> <ListItemIcon><PieChart /></ListItemIcon>
<ListItemText primary="Maths" /> <ListItemText primary="Maths" />
@ -147,6 +153,8 @@ class MusicTools extends Component {
<React.Suspense fallback={<LoadingMessage/>}> <React.Suspense fallback={<LoadingMessage/>}>
<Route path="/app" exact component={LazyIndex} /> <Route path="/app" exact component={LazyIndex} />
<Route path="/app/playlists" component={LazyPlaylists} /> <Route path="/app/playlists" component={LazyPlaylists} />
<Route path="/app/tags" component={LazyTags} />
<Route path="/app/tag/:tag_id" component={LazyTag} />
<Route path="/app/maths" component={LazyMaths} /> <Route path="/app/maths" component={LazyMaths} />
<Route path="/app/settings" component={LazySettings} /> <Route path="/app/settings" component={LazySettings} />
{ this.state.type == 'admin' && <Route path="/app/admin" component={LazyAdmin} /> } { this.state.type == 'admin' && <Route path="/app/admin" component={LazyAdmin} /> }

View File

@ -97,18 +97,24 @@ export class Edit extends Component{
axios.all([this.getPlaylistInfo(), this.getPlaylists()]) axios.all([this.getPlaylistInfo(), this.getPlaylists()])
.then(axios.spread((info, playlists) => { .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; }
if(a.toLowerCase() > b.toLowerCase()) { return 1; } if(a.toLowerCase() > b.toLowerCase()) { return 1; }
return 0; 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; }
if(a.toLowerCase() > b.toLowerCase()) { return 1; } if(a.toLowerCase() > b.toLowerCase()) { return 1; }
return 0; 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); var filteredPlaylists = playlists.data.playlists.filter((entry) => entry.name != this.state.name);
this.setState(info.data); this.setState(info.data);
@ -421,7 +427,7 @@ export class Edit extends Component{
<Grid item xs={8} sm={8} md={3}> <Grid item xs={8} sm={8} md={3}>
<TextField <TextField
name="newPlaylistName" name="newPlaylistName"
variant="outlined" variant="filled"
label="Spotify Playlist" label="Spotify Playlist"
value={this.state.newPlaylistName} value={this.state.newPlaylistName}
onChange={this.handleInputChange} onChange={this.handleInputChange}
@ -476,6 +482,7 @@ export class Edit extends Component{
<TextField type="number" <TextField type="number"
name="recommendation_sample" name="recommendation_sample"
label="Recommendation Size" label="Recommendation Size"
variant="filled"
value={this.state.recommendation_sample} value={this.state.recommendation_sample}
onChange={this.handleInputChange}></TextField> onChange={this.handleInputChange}></TextField>
</Grid> </Grid>
@ -485,6 +492,7 @@ export class Edit extends Component{
<TextField type="number" <TextField type="number"
name="chart_limit" name="chart_limit"
label="Chart Size" label="Chart Size"
variant="filled"
value={this.state.chart_limit} value={this.state.chart_limit}
onChange={this.handleInputChange}></TextField> onChange={this.handleInputChange}></TextField>
</Grid> </Grid>

85
src/js/Tag/New.js Normal file
View File

@ -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 (
<div style={{maxWidth: '500px', margin: 'auto', marginTop: '20px'}}>
<Card align="center">
<CardContent>
<Grid container spacing={5}>
<Grid item xs={12}>
<Typography variant="h3">New Tag</Typography>
</Grid>
<Grid item xs={12}>
<TextField
label="Name"
variant="outlined"
onChange={this.handleInputChange}
name="tag_id"
value={this.state.tag_id}
className="full-width" />
</Grid>
</Grid>
</CardContent>
<CardActions>
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
</CardActions>
</Card>
</div>
);
}
}
export default NewTag;

View File

@ -15,14 +15,12 @@ class TagList extends Component {
isLoading: true isLoading: true
} }
this.getTags(); this.getTags();
this.handleRunTag = this.handleRunTag.bind(this);
this.handleDeleteTag = this.handleDeleteTag.bind(this); this.handleDeleteTag = this.handleDeleteTag.bind(this);
this.handleRunAll = this.handleRunAll.bind(this);
} }
getTags(){ getTags(){
var self = this; var self = this;
axios.get('/api/tags') axios.get('/api/tag')
.then((response) => { .then((response) => {
var tags = response.data.tags.slice(); var tags = response.data.tags.slice();
@ -34,69 +32,29 @@ class TagList extends Component {
}); });
self.setState({ self.setState({
playlists: tags, tags: tags,
isLoading: false isLoading: false
}); });
}) })
.catch((error) => { .catch((error) => {
showMessage(`Error Getting Playlists (${error.response.status})`); showMessage(`Error Getting Tags (${error.response.status})`);
}); });
} }
handleRunTag(name, event){ handleDeleteTag(tag_id, event){
axios.get('/api/user') axios.delete(`/api/tag/${tag_id}`)
.then((response) => { .then((response) => {
if(response.data.spotify_linked == true){ showMessage(`${tag_id} Deleted`);
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`);
this.getTags(); this.getTags();
}).catch((error) => { }).catch((error) => {
showMessage(`Error Deleting ${name} (${error.response.status})`); showMessage(`Error Deleting ${tag_id} (${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})`);
}); });
} }
render() { render() {
const grid = <TagGrid tags={this.state.tags} const grid = <TagGrid tags={this.state.tags}
handleRunTag={this.handleRunTag} handleDeleteTag={this.handleDeleteTag}/>;
handleDeleteTag={this.handleDeleteTag}
handleRunAll={this.handleRunAll}/>;
return this.state.isLoading ? <CircularProgress /> : grid; return this.state.isLoading ? <CircularProgress /> : grid;
} }
@ -116,7 +74,6 @@ function TagGrid(props){
orientation="vertical" orientation="vertical"
className="full-width"> className="full-width">
<Button component={Link} to='tags/new' >New</Button> <Button component={Link} to='tags/new' >New</Button>
<Button onClick={props.handleRunAll}>Run All</Button>
</ButtonGroup> </ButtonGroup>
</Grid> </Grid>
{ props.tags.length == 0 ? ( { props.tags.length == 0 ? (
@ -146,9 +103,8 @@ function TagCard(props){
<ButtonGroup <ButtonGroup
color="primary" color="primary"
variant="contained"> variant="contained">
<Button component={Link} to={getTagLink(props.tag.name)}>View</Button> <Button component={Link} to={getTagLink(props.tag.tag_id)}>View</Button>
<Button onClick={(e) => props.handleRunTag(props.tag.name, e)}>Update</Button> <Button onClick={(e) => props.handleDeleteTag(props.tag.tag_id, e)}>Delete</Button>
<Button onClick={(e) => props.handleDeleteTag(props.tag.name, e)}>Delete</Button>
</ButtonGroup> </ButtonGroup>
</CardActions> </CardActions>
</Card> </Card>
@ -157,7 +113,7 @@ function TagCard(props){
} }
function getTagLink(tagName){ function getTagLink(tagName){
return `/app/tag/${tagName}/edit`; return `/app/tag/${tagName}`;
} }
export default TagList; export default TagList;

20
src/js/Tag/TagRouter.js Normal file
View File

@ -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 (
<div>
<Switch>
<Route exact path={`${this.props.match.url}/`} component={TagList} />
<Route path={`${this.props.match.url}/new`} component={New} />
</Switch>
</div>
);
}
}
export default TagRouter;

389
src/js/Tag/View.js Normal file
View File

@ -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 = (
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
<Card align="center">
<CardContent>
<Typography variant="h2" color="textPrimary">{this.state.tag.name}</Typography>
<Grid container spacing={5}>
{ this.state.tag.artists.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Artists</Typography></Grid> }
{ this.state.tag.artists.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.artists} addType="artists"/> }
{ this.state.tag.albums.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Albums</Typography></Grid> }
{ this.state.tag.albums.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.albums} addType="albums"/> }
{ this.state.tag.tracks.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Tracks</Typography></Grid> }
{ this.state.tag.tracks.length > 0 && <ListBlock handler={this.handleRemoveObj} list={this.state.tag.tracks} addType="tracks"/> }
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
<TextField
name="name"
label="Name"
variant="filled"
value={this.state.name}
onChange={this.handleInputChange}></TextField>
</Grid>
{ this.state.addType != 'artists' &&
<Grid item xs={12} sm={3} md={4}>
<TextField
name="artist"
label="Artist"
variant="filled"
value={this.state.artist}
onChange={this.handleInputChange}></TextField>
</Grid>
}
<Grid item xs={12} sm={this.state.addType != 'artists' ? 2 : 4} md={this.state.addType != 'artists' ? 2 : 4}>
<FormControl>
<InputLabel htmlFor="addType">Type</InputLabel>
<Select
native
value={this.state.addType}
onChange={this.handleInputChange}
inputProps={{
name: "addType",
id: "addType",
}}
>
<option value="artists">Artist</option>
<option value="albums">Album</option>
<option value="tracks">Track</option>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={this.state.addType != 'artists' ? 3 : 4} md={this.state.addType != 'artists' ? 3 : 4}>
<Button variant="contained" onClick={this.handleAdd} className="full-width">Add</Button>
</Grid>
<StatsCard count={this.state.tag.count} proportion={this.state.tag.proportion}></StatsCard>
<Grid item xs={12}>
<PieChart data={data}/>
</Grid>
<Grid item xs={12}>
<BarChart data={data} title='scrobbles'/>
</Grid>
</Grid>
</CardContent>
<CardActions>
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Update</Button>
</CardActions>
</Card>
</div>
);
return this.state.isLoading ? <CircularProgress /> : table;
}
}
export default View;
function ListBlock(props) {
return <Grid container
spacing={3}
direction="row"
justify="flex-start"
alignItems="flex-start"
style={{padding: '24px'}}>
{props.list.map((music_obj) => <BlockGridItem music_obj={ music_obj } key={ music_obj.name }
handler={ props.handler } addType={ props.addType }/>)}
</Grid>
}
function BlockGridItem (props) {
const classes = useStyles();
return (
<Grid item xs={12} sm={4} md={3}>
<Card variant="outlined" className={classes.root}>
<CardContent>
<Grid>
<Grid item xs={12}>
<Typography variant="h4" color="textSecondary" className={classes.root}>{ props.music_obj.name }</Typography>
</Grid>
{ 'artist' in props.music_obj &&
<Grid item xs={12}>
<Typography variant="body1" color="textSecondary" className={classes.root}>{ props.music_obj.artist }</Typography>
</Grid>
}
{ 'count' in props.music_obj &&
<Grid item xs={8}>
<Typography variant="h4" color="textPrimary" className={classes.root}>{ props.music_obj.count }</Typography>
</Grid>
}
</Grid>
</CardContent>
<CardActions>
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(props.music_obj, props.addType, e)} startIcon={<Delete />}>
Delete
</Button>
</CardActions>
</Card>
</Grid>
);
}
function StatsCard (props) {
const classes = useStyles();
return (
<Grid item xs={12}>
<Card variant="outlined" className={classes.root}>
<CardContent>
<Grid container spacing={10}>
<Grid item xs={12}>
<Typography variant="h1" color="textPrimary" className={classes.root}>= { props.count }</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h4" color="textSecondary" className={classes.root}>{ Math.round(props.proportion) }%</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
</Grid>
);
}