added play scratch pad

This commit is contained in:
aj 2019-08-12 00:34:04 +01:00
parent 57408efcda
commit dcfd197d93
8 changed files with 674 additions and 42 deletions

View File

@ -9,3 +9,5 @@ handlers:
script: auto script: auto
secure: always secure: always
env_variables:
DEPLOY_DESTINATION: 'PROD'

View File

@ -1,5 +1,6 @@
from flask import Blueprint, session, request, jsonify from flask import Blueprint, session, request, jsonify
import os
import datetime import datetime
import json import json
@ -10,6 +11,7 @@ from google.protobuf import timestamp_pb2
from werkzeug.security import check_password_hash, generate_password_hash 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.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 import spotify.api.database as database
@ -169,6 +171,9 @@ def playlist():
if playlist_recommendation_sample is not None: if playlist_recommendation_sample is not None:
dic['recommendation_sample'] = playlist_recommendation_sample dic['recommendation_sample'] = playlist_recommendation_sample
if playlist_type is not None:
dic['type'] = playlist_type
if len(dic) == 0: if len(dic) == 0:
return jsonify({"message": 'no changes to make', "status": "error"}), 400 return jsonify({"message": 'no changes to make', "status": "error"}), 400
@ -300,6 +305,76 @@ def change_password():
return jsonify({'error': 'not logged in'}), 401 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']) @blueprint.route('/playlist/run', methods=['GET'])
def run_playlist(): def run_playlist():
@ -309,7 +384,10 @@ def run_playlist():
if playlist_name: 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 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 len(iterate_playlist['parts']) > 0 or len(iterate_playlist['playlist_references']) > 0:
if iterate_playlist.get('playlist_id', None): if iterate_playlist.get('playlist_id', None):
task = { if os.environ.get('DEPLOY_DESTINATION', None) and os.environ['DEPLOY_DESTINATION'] == 'PROD':
'app_engine_http_request': { # Specify the type of request. create_run_user_playlist_task(username, iterate_playlist['name'], seconds_delay)
'http_method': 'POST', else:
'relative_uri': '/api/playlist/run/task', run_playlist(username, iterate_playlist['name'])
'body': json.dumps({
'username': username,
'name': iterate_playlist['name']
}).encode()
}
}
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. def create_run_user_playlist_task(username, playlist_name, delay=0):
task['schedule_time'] = timestamp
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): def push_run_user_playlist_message(username, name):

View File

@ -127,7 +127,7 @@ def auth():
{ {
'client_id': client_id, 'client_id': client_id,
'response_type': 'code', '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' 'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token'
} }
) )

View File

@ -19,12 +19,19 @@ logger.setLevel('INFO')
log_format = '%(levelname)s %(name)s:%(funcName)s - %(message)s' log_format = '%(levelname)s %(name)s:%(funcName)s - %(message)s'
formatter = logging.Formatter(log_format) formatter = logging.Formatter(log_format)
client = glogging.Client() if os.environ.get('DEPLOY_DESTINATION', None) and os.environ['DEPLOY_DESTINATION'] == 'PROD':
handler = CloudLoggingHandler(client) 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 = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), '..', 'build'), template_folder="templates")
app.secret_key = db.collection(u'spotify').document(u'config').get().to_dict()['secret_key'] app.secret_key = db.collection(u'spotify').document(u'config').get().to_dict()['secret_key']

View File

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

View File

@ -13,7 +13,7 @@ class PlaylistView extends Component{
playlists: [], playlists: [],
filteredPlaylists: [], filteredPlaylists: [],
playlist_references: [], playlist_references: [],
type: null, type: 'default',
day_boundary: '', day_boundary: '',
recommendation_sample: '', recommendation_sample: '',
@ -84,6 +84,9 @@ class PlaylistView extends Component{
if(event.target.name == 'recommendation_sample'){ if(event.target.name == 'recommendation_sample'){
this.handleRecSampleChange(event.target.value); this.handleRecSampleChange(event.target.value);
} }
if(event.target.name == 'type'){
this.handleTypeChange(event.target.value);
}
} }
handleDayBoundaryChange(boundary) { 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) { handleShuffleChange(event) {
this.setState({ this.setState({
shuffle: event.target.checked shuffle: event.target.checked
@ -240,22 +252,26 @@ class PlaylistView extends Component{
} }
handleRun(event){ handleRun(event){
axios.get('/api/user') if(this.state.playlist_references.length > 0 || this.state.parts.length > 0){
.then((response) => { axios.get('/api/user')
if(response.data.spotify_linked == true){ .then((response) => {
axios.get('/api/playlist/run', {params: {name: this.state.name}}) if(response.data.spotify_linked == true){
.then((reponse) => { axios.get('/api/playlist/run', {params: {name: this.state.name}})
showMessage(`${this.state.name} ran`); .then((reponse) => {
}) showMessage(`${this.state.name} ran`);
.catch((error) => { })
showMessage(`error running ${this.state.name} (${error.response.status})`); .catch((error) => {
}); showMessage(`error running ${this.state.name} (${error.response.status})`);
}else{ });
showMessage(`link spotify before running`); }else{
} showMessage(`link spotify before running`);
}).catch((error) => { }
showMessage(`error running ${this.state.name} (${error.response.status})`); }).catch((error) => {
}); showMessage(`error running ${this.state.name} (${error.response.status})`);
});
}else{
showMessage(`add either playlists or parts`);
}
} }
render(){ render(){
@ -349,6 +365,20 @@ class PlaylistView extends Component{
</td> </td>
</tr> </tr>
} }
<tr>
<td className="center-text ui-text text-no-select">
playlist type
</td>
<td>
<select className="full-width"
name="type"
onChange={this.handleInputChange}
value={this.state.type}>
<option value="default">default</option>
<option value="recents">recents</option>
</select>
</td>
</tr>
{ this.state.type == 'recents' && { this.state.type == 'recents' &&
<tr> <tr>
<td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}> <td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}>

View File

@ -4,6 +4,7 @@ const axios = require('axios');
import PlaylistsView from "./PlaylistsView.js" import PlaylistsView from "./PlaylistsView.js"
import NewPlaylist from "./NewPlaylist.js"; import NewPlaylist from "./NewPlaylist.js";
import ScratchView from "./ScratchView.js";
class Playlists extends Component { class Playlists extends Component {
render(){ render(){
@ -11,11 +12,13 @@ class Playlists extends Component {
<div> <div>
<ul className="navbar" style={{width: "100%"}}> <ul className="navbar" style={{width: "100%"}}>
<li><Link to={`${this.props.match.url}/new`}>new</Link></li> <li><Link to={`${this.props.match.url}/new`}>new</Link></li>
<li><Link to={`${this.props.match.url}/play`}>play</Link></li>
</ul> </ul>
<Switch> <Switch>
<Route exact path={`${this.props.match.url}/`} component={PlaylistsView} /> <Route exact path={`${this.props.match.url}/`} component={PlaylistsView} />
<Route path={`${this.props.match.url}/new`} component={NewPlaylist} /> <Route path={`${this.props.match.url}/new`} component={NewPlaylist} />
<Route path={`${this.props.match.url}/play`} component={ScratchView} />
</Switch> </Switch>
</div> </div>
); );

View File

@ -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 = (
<table className="app-table max-width">
{/* <thead>
<tr>
<th colSpan="2"><h1 className="text-no-select">{ this.state.name }</h1></th>
</tr>
</thead> */}
{ this.state.playlist_references.length > 0 && <ListBlock name="managed" handler={this.handleRemoveRefRow} list={this.state.playlist_references}/> }
{ this.state.parts.length > 0 && <ListBlock name="spotify" handler={this.handleRemoveRow} list={this.state.parts}/> }
<tbody>
<tr>
<td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}>
<br></br>spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive
</td>
</tr>
<tr>
<td>
<input type="text"
name="newPlaylistName"
className="full-width"
value={this.state.newPlaylistName}
onChange={this.handleInputChange}
placeholder="spotify playlist name"></input>
</td>
<td>
<button className="button full-width" onClick={this.handleAddPart}>add</button>
</td>
</tr>
<tr>
<td>
<select name="newPlaylistReference"
className="full-width"
value={this.state.newPlaylistReference}
onChange={this.handleInputChange}>
{ this.state.playlists
.filter((entry) => entry.name != this.state.name)
.map((entry) => <ReferenceEntry name={entry.name} key={entry.name} />) }
</select>
</td>
<td>
<button className="button full-width" onClick={this.handleAddReference}>add</button>
</td>
</tr>
<tr>
<td className="center-text ui-text text-no-select">
shuffle output?
</td>
<td>
<input type="checkbox"
name="shuffle"
checked={this.state.shuffle}
onChange={this.handleShuffleChange}></input>
</td>
</tr>
<tr>
<td className="center-text ui-text text-no-select">
include recommendations?
</td>
<td>
<input type="checkbox"
name="include_recommendations"
checked={this.state.include_recommendations}
onChange={this.handleRecChange}></input>
</td>
</tr>
<tr>
<td className="center-text ui-text text-no-select">
number of recommendations
</td>
<td>
<input type="number"
name="recommendation_sample"
className="full-width"
value={this.state.recommendation_sample}
onChange={this.handleInputChange}></input>
</td>
</tr>
{ this.state.type == 'recents' &&
<tr>
<td className="center-text ui-text text-no-select">
added since (days)
</td>
<td>
<input type="number"
name="day_boundary"
className="full-width"
value={this.state.day_boundary}
onChange={this.handleInputChange}></input>
</td>
</tr>
}
<tr>
<td className="center-text ui-text text-no-select">
playlist type
</td>
<td>
<select className="full-width"
name="type"
onChange={this.handleInputChange}
value={this.state.type}>
<option value="default">default</option>
<option value="recents">recents</option>
</select>
</td>
</tr>
{ this.state.type == 'recents' &&
<tr>
<td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}>
<br></br>'recents' playlists search for and include this months and last months playlists when named in the format
<br></br>[month] [year]
<br></br>e.g july 19 (lowercase)
</td>
</tr>
}
<tr>
<td colSpan="2">
<button className="button full-width" onClick={this.handleRun}>play</button>
</td>
</tr>
</tbody>
</table>
);
const error = <p style={{textAlign: "center"}}>{ this.state.error_text }</p>;
return this.state.error ? error : table;
}
}
function ReferenceEntry(props) {
return (
<option value={props.name}>{props.name}</option>
);
}
function ListBlock(props) {
return (
<tbody>
<tr><td colSpan="2" className="ui-text center-text text-no-select" style={{fontStyle: 'italic'}}>{props.name}</td></tr>
{ props.list.map((part) => <Row part={ part } key={ part } handler={props.handler}/>) }
</tbody>
);
}
function Row (props) {
return (
<tr>
<td className="ui-text center-text text-no-select">{ props.part }</td>
<td><button className="ui-text center-text button full-width" onClick={(e) => props.handler(props.part, e)}>remove</button></td>
</tr>
);
}
export default ScratchView;