diff --git a/app.yaml b/app.yaml index 65b269a..17a2720 100644 --- a/app.yaml +++ b/app.yaml @@ -9,3 +9,5 @@ handlers: script: auto secure: always +env_variables: + DEPLOY_DESTINATION: 'PROD' \ No newline at end of file diff --git a/spotify/api/api.py b/spotify/api/api.py index d649c96..e9fffe6 100644 --- a/spotify/api/api.py +++ b/spotify/api/api.py @@ -1,5 +1,6 @@ from flask import Blueprint, session, request, jsonify +import os import datetime import json @@ -10,6 +11,7 @@ from google.protobuf import timestamp_pb2 from werkzeug.security import check_password_hash, generate_password_hash from spotify.tasks.run_user_playlist import run_user_playlist as run_user_playlist +from spotify.tasks.play_user_playlist import play_user_playlist as play_user_playlist import spotify.api.database as database @@ -169,6 +171,9 @@ def playlist(): if playlist_recommendation_sample is not None: dic['recommendation_sample'] = playlist_recommendation_sample + if playlist_type is not None: + dic['type'] = playlist_type + if len(dic) == 0: return jsonify({"message": 'no changes to make', "status": "error"}), 400 @@ -300,6 +305,76 @@ def change_password(): return jsonify({'error': 'not logged in'}), 401 +@blueprint.route('/playlist/play', methods=['POST']) +def play_playlist(): + + if 'username' in session: + + request_json = request.get_json() + + request_parts = request_json.get('parts', None) + request_playlist_type = request_json.get('playlist_type', 'default') + request_playlists = request_json.get('playlists', None) + request_shuffle = request_json.get('shuffle', False) + request_include_recommendations = request_json.get('include_recommendations', True) + request_recommendation_sample = request_json.get('recommendation_sample', 10) + request_day_boundary = request_json.get('day_boundary', 10) + + if request_parts or request_playlists: + if len(request_parts) > 0 or len(request_playlists) > 0: + + if os.environ.get('DEPLOY_DESTINATION', None) and os.environ['DEPLOY_DESTINATION'] == 'PROD': + create_play_user_playlist_task(session['username'], + parts=request_parts, + playlist_type=request_playlist_type, + playlists=request_playlists, + shuffle=request_shuffle, + include_recommendations=request_include_recommendations, + recommendation_sample=request_recommendation_sample, + day_boundary=request_day_boundary) + else: + play_user_playlist(session['username'], + parts=request_parts, + playlist_type=request_playlist_type, + playlists=request_playlists, + shuffle=request_shuffle, + include_recommendations=request_include_recommendations, + recommendation_sample=request_recommendation_sample, + day_boundary=request_day_boundary) + + return jsonify({'message': 'execution requested', 'status': 'success'}), 200 + + else: + return jsonify({'error': 'insufficient playlist sources'}), 400 + + else: + return jsonify({'error': 'insufficient playlist sources'}), 400 + + else: + return jsonify({'error': 'not logged in'}), 401 + + +@blueprint.route('/playlist/play/task', methods=['POST']) +def play_playlist_task(): + if request.headers.get('X-AppEngine-QueueName', None): + payload = request.get_data(as_text=True) + if payload: + payload = json.loads(payload) + + play_user_playlist(payload['username'], + parts=payload['parts'], + playlist_type=payload['playlist_type'], + playlists=payload['playlists'], + shuffle=payload['shuffle'], + include_recommendations=payload['include_recommendations'], + recommendation_sample=payload['recommendation_sample'], + day_boundary=payload['day_boundary']) + + return jsonify({'message': 'executed playlist', 'status': 'success'}), 200 + else: + return jsonify({'error': 'unauthorized'}), 401 + + @blueprint.route('/playlist/run', methods=['GET']) def run_playlist(): @@ -309,7 +384,10 @@ def run_playlist(): if playlist_name: - run_user_playlist(session['username'], playlist_name) + if os.environ.get('DEPLOY_DESTINATION', None) and os.environ['DEPLOY_DESTINATION'] == 'PROD': + create_run_user_playlist_task(session['username'], playlist_name) + else: + run_user_playlist(session['username'], playlist_name) return jsonify({'message': 'execution requested', 'status': 'success'}), 200 @@ -432,31 +510,78 @@ def execute_user(username): if len(iterate_playlist['parts']) > 0 or len(iterate_playlist['playlist_references']) > 0: if iterate_playlist.get('playlist_id', None): - 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': iterate_playlist['name'] - }).encode() - } - } + if os.environ.get('DEPLOY_DESTINATION', None) and os.environ['DEPLOY_DESTINATION'] == 'PROD': + create_run_user_playlist_task(username, iterate_playlist['name'], seconds_delay) + else: + run_playlist(username, iterate_playlist['name']) - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds_delay) + seconds_delay += 6 - # Create Timestamp protobuf. - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - # Add the timestamp to the tasks. - task['schedule_time'] = timestamp +def create_run_user_playlist_task(username, playlist_name, delay=0): - tasker.create_task(task_path, task) + 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() + } + } - seconds_delay += 10 + if delay > 0: - # execute_playlist(username, iterate_playlist['name']) + 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, + delay=0): + 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 + }).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 push_run_user_playlist_message(username, name): diff --git a/spotify/auth/auth.py b/spotify/auth/auth.py index 89829d9..e9c6296 100644 --- a/spotify/auth/auth.py +++ b/spotify/auth/auth.py @@ -127,7 +127,7 @@ def auth(): { 'client_id': client_id, 'response_type': 'code', - 'scope': 'playlist-modify-public playlist-modify-private playlist-read-private', + 'scope': 'playlist-modify-public playlist-modify-private playlist-read-private user-modify-playback-state', 'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token' } ) diff --git a/spotify/spotify.py b/spotify/spotify.py index 9e55151..c020f05 100644 --- a/spotify/spotify.py +++ b/spotify/spotify.py @@ -19,12 +19,19 @@ logger.setLevel('INFO') log_format = '%(levelname)s %(name)s:%(funcName)s - %(message)s' formatter = logging.Formatter(log_format) -client = glogging.Client() -handler = CloudLoggingHandler(client) +if os.environ.get('DEPLOY_DESTINATION', None) and os.environ['DEPLOY_DESTINATION'] == 'PROD': + client = glogging.Client() + handler = CloudLoggingHandler(client) -handler.setFormatter(formatter) + handler.setFormatter(formatter) -logger.addHandler(handler) + logger.addHandler(handler) + +else: + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + + logger.addHandler(stream_handler) app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), '..', 'build'), template_folder="templates") app.secret_key = db.collection(u'spotify').document(u'config').get().to_dict()['secret_key'] diff --git a/spotify/tasks/play_user_playlist.py b/spotify/tasks/play_user_playlist.py new file mode 100644 index 0000000..c14ab80 --- /dev/null +++ b/spotify/tasks/play_user_playlist.py @@ -0,0 +1,111 @@ +from google.cloud import firestore + +import datetime +import logging + +from spotframework.engine.playlistengine import PlaylistEngine +from spotframework.engine.filter.shuffle import Shuffle +from spotframework.engine.filter.sortreversereleasedate import SortReverseReleaseDate +from spotframework.engine.filter.deduplicatebyid import DeduplicateByID + +from spotframework.net.network import Network +from spotframework.net.user import User + +db = firestore.Client() + +captured_playlists = [] + + +def play_user_playlist(username, + playlist_type='default', + parts=None, + playlists=None, + shuffle=False, + include_recommendations=True, + recommendation_sample=10, + day_boundary=10): + + logger = logging.getLogger(__name__) + + users = [i for i in db.collection(u'spotify_users').where(u'username', u'==', username).stream()] + + logger.info(f'{username}') + + if len(users) == 1: + + user_dict = users[0].to_dict() + + if not parts and not playlists: + logger.critical(f'no playlists to use for creation ({username})') + return + + if len(parts) == 0 and len(playlists) == 0: + logger.critical(f'no playlists to use for creation ({username})') + return + + spotify_keys = db.document('key/spotify').get().to_dict() + + net = Network(User(spotify_keys['clientid'], + spotify_keys['clientsecret'], + user_dict['access_token'], + user_dict['refresh_token'])) + + engine = PlaylistEngine(net) + engine.load_user_playlists() + + processors = [DeduplicateByID()] + + if shuffle: + processors.append(Shuffle()) + else: + processors.append(SortReverseReleaseDate()) + + global captured_playlists + captured_playlists = [] + + if not parts: + parts = [] + + submit_parts = parts + + for part in playlists: + submit_parts += generate_parts(users[0].id, part) + + submit_parts = [i for i in {j for j in submit_parts}] + + if playlist_type == 'recents': + boundary_date = datetime.datetime.now() - datetime.timedelta(days=int(day_boundary)) + tracks = engine.get_recent_playlist(boundary_date, + submit_parts, + processors, + include_recommendations=include_recommendations, + recommendation_limit=int(recommendation_sample)) + else: + tracks = engine.make_playlist(submit_parts, + processors, + include_recommendations=include_recommendations, + recommendation_limit=int(recommendation_sample)) + + net.play(uris=[i['uri'] for i in tracks]) + + else: + logger.critical(f'multiple/no user(s) found ({username})') + return + + +def generate_parts(user_id, name): + + playlist_doc = [i.to_dict() for i in + db.document(u'spotify_users/{}'.format(user_id)) + .collection(u'playlists') + .where(u'name', '==', name).stream()][0] + + return_parts = playlist_doc['parts'] + + captured_playlists.append(name) + + for i in playlist_doc['playlist_references']: + if i not in captured_playlists: + return_parts += generate_parts(user_id, i) + + return return_parts diff --git a/src/js/Playlist/PlaylistView.js b/src/js/Playlist/PlaylistView.js index 222d107..0f86f17 100644 --- a/src/js/Playlist/PlaylistView.js +++ b/src/js/Playlist/PlaylistView.js @@ -13,7 +13,7 @@ class PlaylistView extends Component{ playlists: [], filteredPlaylists: [], playlist_references: [], - type: null, + type: 'default', day_boundary: '', recommendation_sample: '', @@ -84,6 +84,9 @@ class PlaylistView extends Component{ if(event.target.name == 'recommendation_sample'){ this.handleRecSampleChange(event.target.value); } + if(event.target.name == 'type'){ + this.handleTypeChange(event.target.value); + } } handleDayBoundaryChange(boundary) { @@ -104,6 +107,15 @@ class PlaylistView extends Component{ }); } + handleTypeChange(sample){ + axios.post('/api/playlist', { + name: this.state.name, + type: sample + }).catch((error) => { + showMessage(`error updating type (${error.response.status})`); + }); + } + handleShuffleChange(event) { this.setState({ shuffle: event.target.checked @@ -240,22 +252,26 @@ class PlaylistView extends Component{ } handleRun(event){ - axios.get('/api/user') - .then((response) => { - if(response.data.spotify_linked == true){ - axios.get('/api/playlist/run', {params: {name: this.state.name}}) - .then((reponse) => { - showMessage(`${this.state.name} ran`); - }) - .catch((error) => { - showMessage(`error running ${this.state.name} (${error.response.status})`); - }); - }else{ - showMessage(`link spotify before running`); - } - }).catch((error) => { - showMessage(`error running ${this.state.name} (${error.response.status})`); - }); + if(this.state.playlist_references.length > 0 || this.state.parts.length > 0){ + axios.get('/api/user') + .then((response) => { + if(response.data.spotify_linked == true){ + axios.get('/api/playlist/run', {params: {name: this.state.name}}) + .then((reponse) => { + showMessage(`${this.state.name} ran`); + }) + .catch((error) => { + showMessage(`error running ${this.state.name} (${error.response.status})`); + }); + }else{ + showMessage(`link spotify before running`); + } + }).catch((error) => { + showMessage(`error running ${this.state.name} (${error.response.status})`); + }); + }else{ + showMessage(`add either playlists or parts`); + } } render(){ @@ -349,6 +365,20 @@ class PlaylistView extends Component{ } + + + playlist type + + + + + { this.state.type == 'recents' && diff --git a/src/js/Playlist/Playlists.js b/src/js/Playlist/Playlists.js index 9312d27..136ea41 100644 --- a/src/js/Playlist/Playlists.js +++ b/src/js/Playlist/Playlists.js @@ -4,6 +4,7 @@ const axios = require('axios'); import PlaylistsView from "./PlaylistsView.js" import NewPlaylist from "./NewPlaylist.js"; +import ScratchView from "./ScratchView.js"; class Playlists extends Component { render(){ @@ -11,11 +12,13 @@ class Playlists extends Component {
+
); diff --git a/src/js/Playlist/ScratchView.js b/src/js/Playlist/ScratchView.js new file mode 100644 index 0000000..532860e --- /dev/null +++ b/src/js/Playlist/ScratchView.js @@ -0,0 +1,354 @@ +import React, { Component } from "react"; +const axios = require('axios'); + +import showMessage from "../Toast.js" + +class ScratchView extends Component{ + + constructor(props){ + super(props); + this.state = { + name: 'play', + parts: [], + playlists: [], + filteredPlaylists: [], + playlist_references: [], + type: 'default', + + day_boundary: 5, + recommendation_sample: 5, + newPlaylistName: '', + newPlaylistReference: '', + + shuffle: false, + include_recommendations: false + } + this.handleAddPart = this.handleAddPart.bind(this); + this.handleAddReference = this.handleAddReference.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleRemoveRow = this.handleRemoveRow.bind(this); + this.handleRemoveRefRow = this.handleRemoveRefRow.bind(this); + + this.handleRun = this.handleRun.bind(this); + + this.handleShuffleChange = this.handleShuffleChange.bind(this); + this.handleRecChange = this.handleRecChange.bind(this); + } + + componentDidMount(){ + + this.getPlaylists(); + } + + + getPlaylists(){ + return axios.get(`/api/playlists`) + .then((response) => { + var filteredPlaylists = response.data.playlists.filter((entry) => entry.name != this.state.name); + + this.setState({ + playlists: response.data.playlists, + newPlaylistReference: filteredPlaylists.length > 0 ? filteredPlaylists[0].name : '' + }); + }) + .catch((error) => { + showMessage(`error getting playlists (${error.response.status})`); + }); + } + + handleInputChange(event){ + this.setState({ + [event.target.name]: event.target.value + }); + } + + handleTypeChange(sample){ + axios.post('/api/playlist', { + name: this.state.name, + type: sample + }).catch((error) => { + showMessage(`error updating type (${error.response.status})`); + }); + } + + handleShuffleChange(event) { + this.setState({ + shuffle: event.target.checked + }); + } + + handleRecChange(event) { + this.setState({ + include_recommendations: event.target.checked + }); + } + + handleAddPart(event){ + + if(this.state.newPlaylistName.length != 0){ + + var check = this.state.parts.includes(this.state.newPlaylistName); + + if(check == false) { + var parts = this.state.parts.slice(); + parts.push(this.state.newPlaylistName); + + parts.sort(function(a, b){ + if(a < b) { return -1; } + if(a > b) { return 1; } + return 0; + }); + + this.setState({ + parts: parts, + newPlaylistName: '' + }); + }else{ + showMessage('playlist already added'); + } + + }else{ + showMessage('enter playlist name'); + } + } + + handleAddReference(event){ + + if(this.state.newPlaylistReference.length != 0){ + + var check = this.state.playlist_references.includes(this.state.newPlaylistReference); + + if(check == false) { + var playlist_references = this.state.playlist_references.slice(); + playlist_references.push(this.state.newPlaylistReference); + + playlist_references.sort(function(a, b){ + if(a < b) { return -1; } + if(a > b) { return 1; } + return 0; + }); + + var filteredPlaylists = this.state.playlists.filter((entry) => entry.name != this.state.name); + + this.setState({ + playlist_references: playlist_references, + newPlaylistReference: filteredPlaylists.length > 0 ? filteredPlaylists[0].name : '' + }); + + }else{ + showMessage('playlist already added'); + } + + }else{ + showMessage('no other playlists to add'); + } + } + + handleRemoveRow(id, event){ + var parts = this.state.parts; + parts = parts.filter(e => e !== id); + this.setState({ + parts: parts + }); + + if(parts.length == 0) { + parts = -1; + } + } + + handleRemoveRefRow(id, event){ + var playlist_references = this.state.playlist_references; + playlist_references = playlist_references.filter(e => e !== id); + this.setState({ + playlist_references: playlist_references + }); + } + + handleRun(event){ + if(this.state.playlist_references.length > 0 || this.state.parts.length > 0){ + axios.get('/api/user') + .then((response) => { + if(response.data.spotify_linked == true){ + axios.post('/api/playlist/play', { + parts: this.state.parts, + playlists: this.state.playlist_references, + shuffle: this.state.shuffle, + include_recommendations: this.state.include_recommendations, + recommendation_sample: this.state.recommendation_sample, + day_boundary: this.state.day_boundary, + playlist_type: this.state.type + }) + .then((reponse) => { + showMessage(`played`); + }) + .catch((error) => { + showMessage(`error playing (${error.response.status})`); + }); + }else{ + showMessage(`link spotify before running`); + } + }).catch((error) => { + showMessage(`error playing (${error.response.status})`); + }); + }else{ + showMessage(`add either playlists or parts`); + } + } + + render(){ + + const table = ( + + {/* + + + + */} + { this.state.playlist_references.length > 0 && } + { this.state.parts.length > 0 && } + + + + + + + + + + + + + + + + + + + + + + + + + { this.state.type == 'recents' && + + + + + } + + + + + { this.state.type == 'recents' && + + + + } + + + + +

{ this.state.name }

+

spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive +
+ + + +
+ + + +
+ shuffle output? + + +
+ include recommendations? + + +
+ number of recommendations + + +
+ added since (days) + + +
+ playlist type + + +
+

'recents' playlists search for and include this months and last months playlists when named in the format +

[month] [year] +

e.g july 19 (lowercase) +
+ +
+ ); + + const error =

{ this.state.error_text }

; + + return this.state.error ? error : table; + } + +} + +function ReferenceEntry(props) { + return ( + + ); +} + +function ListBlock(props) { + return ( + + {props.name} + { props.list.map((part) => ) } + + ); +} + +function Row (props) { + return ( + + { props.part } + + + ); +} + +export default ScratchView; \ No newline at end of file