add and delete playlists, spotify auth

This commit is contained in:
aj 2019-07-31 20:31:01 +01:00
parent e3615d0ccf
commit 84bbcc21fc
15 changed files with 259 additions and 51 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
[playlist manager](https://spotify.sarsoo.xyz)
==================
playlist managing web app acting as a front-end for the [pyfmframework](https://github.com/Sarsoo/pyspotframework) playlist engine

View File

@ -27,15 +27,15 @@ def get_playlists():
return jsonify({'error': 'not logged in'}), 401 return jsonify({'error': 'not logged in'}), 401
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT']) @blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE'])
def get_playlist(): def playlist():
if 'username' in session: if 'username' in session:
user_ref = database.get_user_doc_ref(session['username']) user_ref = database.get_user_doc_ref(session['username'])
playlists = database.get_user_playlists_collection(user_ref.id) playlists = database.get_user_playlists_collection(user_ref.id)
if request.method == 'GET': if request.method == 'GET' or request.method == 'DELETE':
playlist_name = request.args.get('name', None) playlist_name = request.args.get('name', None)
if playlist_name: if playlist_name:
@ -47,8 +47,16 @@ def get_playlist():
elif len(queried_playlist) > 1: elif len(queried_playlist) > 1:
return jsonify({'error': 'multiple playlists found'}), 500 return jsonify({'error': 'multiple playlists found'}), 500
if request.method == "GET":
return jsonify(queried_playlist[0].to_dict()), 200 return jsonify(queried_playlist[0].to_dict()), 200
elif request.method == 'DELETE':
playlists.document(queried_playlist[0].id).delete()
return jsonify({"message": 'playlist deleted', "status": "success"}), 200
else: else:
return jsonify({"error": 'no name requested'}), 400 return jsonify({"error": 'no name requested'}), 400
@ -62,6 +70,7 @@ def get_playlist():
playlist_name = request_json['name'] playlist_name = request_json['name']
playlist_parts = request_json.get('parts', None) playlist_parts = request_json.get('parts', None)
playlist_id = request_json.get('id', None) playlist_id = request_json.get('id', None)
playlist_shuffle = request_json.get('shuffle', None)
queried_playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()] queried_playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()]
@ -70,18 +79,19 @@ def get_playlist():
if len(queried_playlist) != 0: if len(queried_playlist) != 0:
return jsonify({'error': 'playlist already exists'}), 400 return jsonify({'error': 'playlist already exists'}), 400
if playlist_parts is None or playlist_id is None: # if playlist_id is None or playlist_shuffle is None:
return jsonify({'error': 'parts and id required'}), 400 # return jsonify({'error': 'parts and id required'}), 400
playlists.add({ playlists.add({
'name': playlist_name, 'name': playlist_name,
'parts': playlist_parts, 'parts': playlist_parts,
'playlist_id': playlist_id 'playlist_id': playlist_id,
'shuffle': playlist_shuffle
}) })
return jsonify({"message": 'playlist added', "status": "success"}), 200 return jsonify({"message": 'playlist added', "status": "success"}), 200
else: elif request.method == 'POST':
if len(queried_playlist) == 0: if len(queried_playlist) == 0:
return jsonify({'error': "playlist doesn't exist"}), 400 return jsonify({'error': "playlist doesn't exist"}), 400
@ -89,7 +99,7 @@ def get_playlist():
if len(queried_playlist) > 1: if len(queried_playlist) > 1:
return jsonify({'error': "multiple playlists exist"}), 500 return jsonify({'error': "multiple playlists exist"}), 500
if playlist_parts is None and playlist_id is None: if playlist_parts is None and playlist_id is None and playlist_shuffle is None:
return jsonify({'error': "no chnages to make"}), 400 return jsonify({'error': "no chnages to make"}), 400
playlist_doc = playlists.document(queried_playlist[0].id) playlist_doc = playlists.document(queried_playlist[0].id)
@ -102,10 +112,15 @@ def get_playlist():
if playlist_id: if playlist_id:
dic['playlist_id'] = playlist_id dic['playlist_id'] = playlist_id
if playlist_shuffle is not None:
dic['shuffle'] = playlist_shuffle
playlist_doc.update(dic) playlist_doc.update(dic)
return jsonify({"message": 'playlist updated', "status": "success"}), 200 return jsonify({"message": 'playlist updated', "status": "success"}), 200
else: else:
return jsonify({'error': 'not logged in'}), 401 return jsonify({'error': 'not logged in'}), 401

View File

@ -78,7 +78,7 @@ def auth():
return redirect(urllib.parse.urlunparse(['https', 'accounts.spotify.com', 'authorize', '', params, ''])) return redirect(urllib.parse.urlunparse(['https', 'accounts.spotify.com', 'authorize', '', params, '']))
return redirect('/') return redirect(url_for('index'))
@blueprint.route('/spotify/token') @blueprint.route('/spotify/token')
@ -116,7 +116,7 @@ def token():
return redirect('/app/settings/spotify') return redirect('/app/settings/spotify')
return redirect('/') return redirect(url_for('index'))
@blueprint.route('/spotify/deauth') @blueprint.route('/spotify/deauth')
@ -134,4 +134,4 @@ def deauth():
return redirect('/app/settings/spotify') return redirect('/app/settings/spotify')
return redirect('/') return redirect(url_for('index'))

View File

@ -23,9 +23,5 @@
<div id="react"></div> <div id="react"></div>
<script src="{{ url_for('static', filename='js/app.bundle.js') }}"></script> <script src="{{ url_for('static', filename='js/app.bundle.js') }}"></script>
<footer>
<a href="https://github.com/Sarsoo/spotify-web">view source code</a>
</footer>
</body> </body>
</html> </html>

View File

@ -30,7 +30,7 @@
<p class="center-text">password<br><input type="password" name="password" class="full-width"></p> <p class="center-text">password<br><input type="password" name="password" class="full-width"></p>
<p id="status" style="display: none; color: red" class="center-text"></p> <p id="status" style="display: none; color: red" class="center-text"></p>
<button class="full-width" onclick="handleLogin()" type="submit">go</button> <button class="button full-width" onclick="handleLogin()" type="submit">go</button>
</form> </form>
<script src="{{ url_for('static', filename='js/login.bundle.js') }}"></script> <script src="{{ url_for('static', filename='js/login.bundle.js') }}"></script>
</div> </div>

View File

@ -22,7 +22,7 @@ class Index extends Component{
} }
render(){ render(){
return <h1 className="center-text">welcome to playlist manager!</h1>; return <h1 className="center-text text-no-select">welcome to playlist manager!</h1>;
} }
} }

View File

@ -0,0 +1,93 @@
import React, { Component } from "react";
const axios = require('axios');
class NewPlaylist extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
type: 'normal',
error: false,
errorText: null
}
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleInputChange(event){
this.setState({
[event.target.name]: event.target.value
});
}
handleSubmit(event){
axios.get('/api/playlists')
.then((response) => {
var sameName = response.data.playlists.filter((i) => {i.name == this.state.name ? true : false});
if(sameName.length == 0){
axios.put('/api/playlist', {
name: this.state.name,
parts: [],
shuffle: false,
type: this.state.type,
}).catch((error) => {
console.log(error);
})
}else{
this.setState({
error: true,
errorText: 'name already exists'
});
}
});
console.log(this.state);
}
render(){
return (
<table className="app-table">
<thead>
<tr>
<th colSpan="2">
<h1 className="ui-text center-text">new playlist</h1>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select className="full-width" name="type" onChange={this.handleInputChange}>
<option value="normal">normal</option>
<option value="recents">recents</option>
</select>
</td>
<td>
<input
className="full-width"
name="name"
type="text"
value={this.state.name}
onChange={this.handleInputChange}
placeholder="name"/>
</td>
</tr>
<tr>
<td colSpan="2">
<button className="button full-width" onClick={this.handleSubmit}>create</button>
</td>
</tr>
{ this.state.error &&
<tr>
<td colSpan="2">
<p className="full-width" style={{color: 'red'}}>{this.state.errorText}</p>
</td>
</tr>}
</tbody>
</table>
);
}
}
export default NewPlaylist;

View File

@ -5,16 +5,19 @@ class PlaylistView extends Component{
constructor(props){ constructor(props){
super(props); super(props);
console.log(this.props.match.params.name);
this.state = { this.state = {
name: this.props.match.params.name, name: this.props.match.params.name,
parts: [], parts: [],
error: false, error: false,
error_text: null, error_text: null,
add_part_value: '' newPlaylistName: '',
shuffle: false
} }
this.handleAddPart = this.handleAddPart.bind(this); this.handleAddPart = this.handleAddPart.bind(this);
this.handleAddPartChange = this.handleAddPartChange.bind(this); this.handleAddPartChange = this.handleAddPartChange.bind(this);
this.handleRemoveRow = this.handleRemoveRow.bind(this);
this.handleShuffleChange = this.handleShuffleChange.bind(this);
} }
componentDidMount(){ componentDidMount(){
@ -35,21 +38,46 @@ class PlaylistView extends Component{
handleAddPartChange(event){ handleAddPartChange(event){
this.setState({ this.setState({
add_part_value: event.target.value newPlaylistName: event.target.value
});
}
handleShuffleChange(event) {
this.setState({
shuffle: event.target.checked
});
axios.post('/api/playlist', {
name: this.state.name,
shuffle: event.target.checked
}).catch((error) => {
console.log(error);
}); });
} }
handleAddPart(event){ handleAddPart(event){
var parts = this.state.parts; var parts = this.state.parts;
parts.push(this.state.add_part_value); parts.push(this.state.newPlaylistName);
this.setState({
parts: parts,
add_part_value: ''
});
axios.post('/api/playlist', {
name: this.state.name,
parts: parts
}).catch((error) => {
console.log(error);
});
}
handleRemoveRow(id, event){
var parts = this.state.parts;
parts = parts.filter(e => e !== id);
this.setState({ this.setState({
parts: parts parts: parts
}); });
axios.post('/api/playlist', { axios.post('/api/playlist', {
name: this.state.name, name: this.state.name,
parts: parts parts: parts
}).then((response) => {
console.log(reponse);
}).catch((error) => { }).catch((error) => {
console.log(error); console.log(error);
}); });
@ -61,19 +89,33 @@ class PlaylistView extends Component{
<table className="app-table max-width"> <table className="app-table max-width">
<thead> <thead>
<tr> <tr>
<th colSpan="2"><h1>{ this.state.name }</h1></th> <th colSpan="2"><h1 className="text-no-select">{ this.state.name }</h1></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ this.state.parts.map((part) => <Row part={ part } key={ part }/>) } { this.state.parts.map((part) => <Row part={ part } key={ part } handler={this.handleRemoveRow}/>) }
<tr> <tr>
<td> <td>
<input type="text" className="full-width" value={this.state.add_part_value} onChange={this.handleAddPartChange}></input> <input type="text"
className="full-width"
value={this.state.newPlaylistName}
onChange={this.handleAddPartChange}
placeholder="new playlist"></input>
</td> </td>
<td> <td>
<button className="button full-width" onClick={this.handleAddPart}>add</button> <button className="button full-width" onClick={this.handleAddPart}>add</button>
</td> </td>
</tr> </tr>
<tr>
<td className="center-text ui-text text-no-select">
shuffle output?
</td>
<td>
<input type="checkbox"
checked={this.state.shuffle}
onChange={this.handleShuffleChange}></input>
</td>
</tr>
</tbody> </tbody>
</table> </table>
); );
@ -88,8 +130,8 @@ class PlaylistView extends Component{
function Row (props) { function Row (props) {
return ( return (
<tr> <tr>
<td className="ui-text center-text">{ props.part }</td> <td className="ui-text center-text text-no-select">{ props.part }</td>
<td className="ui-text center-text">remove</td> <td><button className="ui-text center-text button full-width" onClick={(e) => props.handler(props.part, e)}>remove</button></td>
</tr> </tr>
); );
} }

View File

@ -1,19 +1,22 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom"; import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
const axios = require('axios'); const axios = require('axios');
import PlaylistsView from "./PlaylistsView.js" import PlaylistsView from "./PlaylistsView.js"
import NewPlaylist from "./NewPlaylist.js";
class Playlists extends Component { class Playlists extends Component {
render(){ render(){
return ( return (
<div> <div>
<ul className="navbar" style={{width: "100%"}}> <ul className="navbar" style={{width: "100%"}}>
<li><Link to={`${this.props.match.url}/add`}>add</Link></li> <li><Link to={`${this.props.match.url}/new`}>new</Link></li>
</ul> </ul>
<Route path={`${this.props.match.url}/`} component={PlaylistsView} /> <Switch>
<Route exact path={`${this.props.match.url}/`} component={PlaylistsView} />
<Route path={`${this.props.match.url}/new`} component={NewPlaylist} />
</Switch>
</div> </div>
); );
} }

View File

@ -10,6 +10,8 @@ class PlaylistsView extends Component {
isLoading: true isLoading: true
} }
this.getPlaylists(); this.getPlaylists();
this.handleRunPlaylist = this.handleRunPlaylist.bind(this);
this.handleDeletePlaylist = this.handleDeletePlaylist.bind(this);
} }
getPlaylists(){ getPlaylists(){
@ -23,9 +25,27 @@ class PlaylistsView extends Component {
}); });
} }
handleRunPlaylist(name, event){
}
handleDeletePlaylist(name, event){
axios.delete('/api/playlist', { params: { name: name } })
.then((response) => {
this.getPlaylists();
}).catch((error) => {
console.log(error);
});
}
render() { render() {
const table = <div><Table playlists={this.state.playlists}/></div>; const table = <div>
<Table playlists={this.state.playlists}
handleRunPlaylist={this.handleRunPlaylist}
handleDeletePlaylist={this.handleDeletePlaylist}/>
</div>;
const loadingMessage = <p className="center-text">loading...</p>; const loadingMessage = <p className="center-text">loading...</p>;
return this.state.isLoading ? loadingMessage : table; return this.state.isLoading ? loadingMessage : table;
@ -36,7 +56,10 @@ function Table(props){
return ( return (
<table className="app-table max-width"> <table className="app-table max-width">
<tbody> <tbody>
{ props.playlists.map((playlist) => <Row playlist={ playlist } key={ playlist.name }/>) } { props.playlists.map((playlist) => <Row playlist={ playlist }
handleRunPlaylist={props.handleRunPlaylist}
handleDeletePlaylist={props.handleDeletePlaylist}
key={ playlist.name }/>) }
</tbody> </tbody>
</table> </table>
); );
@ -46,6 +69,8 @@ function Row(props){
return ( return (
<tr> <tr>
<PlaylistLink playlist={props.playlist}/> <PlaylistLink playlist={props.playlist}/>
<td style={{width: "100px"}}><button className="button" style={{width: "100px"}} onClick={(e) => props.handleRunPlaylist(props.playlist.name, e)}>run</button></td>
<td style={{width: "100px"}}><button className="button" style={{width: "100px"}} onClick={(e) => props.handleDeletePlaylist(props.playlist.name, e)}>delete</button></td>
</tr> </tr>
); );
} }

View File

@ -62,15 +62,17 @@ class PlaylistManager extends Component {
<div className="pad-9"> <div className="pad-9">
<Switch> <Switch>
<Route path="/app" exact component={Index} /> <Route path="/app" exact component={Index} />
<Route path="/app/playlists" exact component={Playlists} /> <Route path="/app/playlists" component={Playlists} />
<Route path="/app/settings" render={(props) => <Settings {...props} spotify_linked={this.state.spotify_linked}/>} /> <Route path="/app/settings" component={Settings} />
{ this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> } { this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> }
<Route path='/app/playlist/:name' component={PlaylistView} /> <Route path='/app/playlist/:name' component={PlaylistView} />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
</div> </div>
</div> </div>
<footer>
<a href="https://github.com/Sarsoo/spotify-web">view source code</a>
</footer>
</Router> </Router>
); );
} }

View File

@ -84,12 +84,12 @@ class ChangePassword extends Component {
<table className="app-table max-width"> <table className="app-table max-width">
<thead> <thead>
<tr> <tr>
<th colSpan="2"><h1>change password</h1></th> <th colSpan="2"><h1 className="text-no-select">change password</h1></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td className="ui-text center-text">current:</td> <td className="ui-text center-text text-no-select">current:</td>
<td><input <td><input
type="password" type="password"
name="current" name="current"
@ -98,7 +98,7 @@ class ChangePassword extends Component {
className="full-width" /></td> className="full-width" /></td>
</tr> </tr>
<tr> <tr>
<td className="ui-text center-text">new:</td> <td className="ui-text center-text text-no-select">new:</td>
<td><input <td><input
type="password" type="password"
name="new1" name="new1"
@ -107,7 +107,7 @@ class ChangePassword extends Component {
className="full-width" /></td> className="full-width" /></td>
</tr> </tr>
<tr> <tr>
<td className="ui-text center-text">new again:</td> <td className="ui-text center-text text-no-select">new again:</td>
<td><input <td><input
type="password" type="password"
name="new2" name="new2"

View File

@ -15,7 +15,7 @@ class Settings extends Component {
</ul> </ul>
<Route path={`${this.props.match.url}/password`} component={ChangePassword} /> <Route path={`${this.props.match.url}/password`} component={ChangePassword} />
<Route path={`${this.props.match.url}/spotify`} render={(props) => <SpotifyLink {...props} spotify_linked={this.props.spotify_linked}/>} /> <Route path={`${this.props.match.url}/spotify`} component={SpotifyLink} />
</div> </div>
); );

View File

@ -6,25 +6,33 @@ class SpotifyLink extends Component {
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
spotify_linked: props.spotify_linked spotify_linked: null,
isLoading: true
} }
this.getUserInfo();
} }
getUserInfo(){ getUserInfo(){
axios.get('/api/user')
.then((response) => {
this.setState({
spotify_linked: response.data.spotify_linked,
isLoading: false
})
});
} }
render(){ render(){
return ( const table =
<table className="app-table max-width"> <table className="app-table max-width">
<thead> <thead>
<tr> <tr>
<th><h1 className="ui-text center-text">spotify link status</h1></th> <th><h1 className="ui-text center-text text-no-select">spotify link status</h1></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td className="ui-text center-text"> <td className="ui-text center-text text-no-select">
status: { this.state.spotify_linked ? "linked" : "unlinked" } status: { this.state.spotify_linked ? "linked" : "unlinked" }
</td> </td>
</tr> </tr>
@ -34,8 +42,11 @@ class SpotifyLink extends Component {
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>;
);
const loadingMessage = <p className="center-text text-no-select">loading...</p>;
return this.state.isLoading ? loadingMessage : table;
} }
} }

View File

@ -70,6 +70,14 @@ p {
} }
} }
input[type=text], input[type=password], select {
padding: 10px;
background-color: #505050;
border: black;
border-radius: 4px;
color: white;
}
.row { .row {
margin: 30px; margin: 30px;
} }
@ -273,7 +281,7 @@ ul.navbar {
width: 100%; width: 100%;
margin: auto; margin: auto;
td { td {
padding: $pad-px; padding: 5px;
} }
} }
@ -281,6 +289,14 @@ ul.navbar {
max-width: 800px; max-width: 800px;
} }
.text-no-select {
cursor: default;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
footer { footer {
p { p {
text-align: right; text-align: right;