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

View File

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

View File

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

View File

@ -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/<tag_id>/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

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}')
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])
})

View File

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

View File

@ -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
}
}]
}
}
});

View File

@ -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 {
<ListItemIcon><QueueMusic /></ListItemIcon>
<ListItemText primary="Playlists" />
</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'>
<ListItemIcon><PieChart /></ListItemIcon>
<ListItemText primary="Maths" />
@ -147,6 +153,8 @@ class MusicTools extends Component {
<React.Suspense fallback={<LoadingMessage/>}>
<Route path="/app" exact component={LazyIndex} />
<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/settings" component={LazySettings} />
{ 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()])
.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{
<Grid item xs={8} sm={8} md={3}>
<TextField
name="newPlaylistName"
variant="outlined"
variant="filled"
label="Spotify Playlist"
value={this.state.newPlaylistName}
onChange={this.handleInputChange}
@ -476,6 +482,7 @@ export class Edit extends Component{
<TextField type="number"
name="recommendation_sample"
label="Recommendation Size"
variant="filled"
value={this.state.recommendation_sample}
onChange={this.handleInputChange}></TextField>
</Grid>
@ -485,6 +492,7 @@ export class Edit extends Component{
<TextField type="number"
name="chart_limit"
label="Chart Size"
variant="filled"
value={this.state.chart_limit}
onChange={this.handleInputChange}></TextField>
</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
}
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 = <TagGrid tags={this.state.tags}
handleRunTag={this.handleRunTag}
handleDeleteTag={this.handleDeleteTag}
handleRunAll={this.handleRunAll}/>;
handleDeleteTag={this.handleDeleteTag}/>;
return this.state.isLoading ? <CircularProgress /> : grid;
}
@ -116,7 +74,6 @@ function TagGrid(props){
orientation="vertical"
className="full-width">
<Button component={Link} to='tags/new' >New</Button>
<Button onClick={props.handleRunAll}>Run All</Button>
</ButtonGroup>
</Grid>
{ props.tags.length == 0 ? (
@ -146,9 +103,8 @@ function TagCard(props){
<ButtonGroup
color="primary"
variant="contained">
<Button component={Link} to={getTagLink(props.tag.name)}>View</Button>
<Button onClick={(e) => props.handleRunTag(props.tag.name, e)}>Update</Button>
<Button onClick={(e) => props.handleDeleteTag(props.tag.name, e)}>Delete</Button>
<Button component={Link} to={getTagLink(props.tag.tag_id)}>View</Button>
<Button onClick={(e) => props.handleDeleteTag(props.tag.tag_id, e)}>Delete</Button>
</ButtonGroup>
</CardActions>
</Card>
@ -157,7 +113,7 @@ function TagCard(props){
}
function getTagLink(tagName){
return `/app/tag/${tagName}/edit`;
return `/app/tag/${tagName}`;
}
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>
);
}