add and delete playlists, spotify auth
This commit is contained in:
parent
e3615d0ccf
commit
84bbcc21fc
5
README.md
Normal file
5
README.md
Normal 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
|
||||
|
@ -27,15 +27,15 @@ def get_playlists():
|
||||
return jsonify({'error': 'not logged in'}), 401
|
||||
|
||||
|
||||
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT'])
|
||||
def get_playlist():
|
||||
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
def playlist():
|
||||
|
||||
if 'username' in session:
|
||||
|
||||
user_ref = database.get_user_doc_ref(session['username'])
|
||||
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)
|
||||
|
||||
if playlist_name:
|
||||
@ -47,8 +47,16 @@ def get_playlist():
|
||||
elif len(queried_playlist) > 1:
|
||||
return jsonify({'error': 'multiple playlists found'}), 500
|
||||
|
||||
if request.method == "GET":
|
||||
|
||||
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:
|
||||
return jsonify({"error": 'no name requested'}), 400
|
||||
|
||||
@ -62,6 +70,7 @@ def get_playlist():
|
||||
playlist_name = request_json['name']
|
||||
playlist_parts = request_json.get('parts', 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()]
|
||||
|
||||
@ -70,18 +79,19 @@ def get_playlist():
|
||||
if len(queried_playlist) != 0:
|
||||
return jsonify({'error': 'playlist already exists'}), 400
|
||||
|
||||
if playlist_parts is None or playlist_id is None:
|
||||
return jsonify({'error': 'parts and id required'}), 400
|
||||
# if playlist_id is None or playlist_shuffle is None:
|
||||
# return jsonify({'error': 'parts and id required'}), 400
|
||||
|
||||
playlists.add({
|
||||
'name': playlist_name,
|
||||
'parts': playlist_parts,
|
||||
'playlist_id': playlist_id
|
||||
'playlist_id': playlist_id,
|
||||
'shuffle': playlist_shuffle
|
||||
})
|
||||
|
||||
return jsonify({"message": 'playlist added', "status": "success"}), 200
|
||||
|
||||
else:
|
||||
elif request.method == 'POST':
|
||||
|
||||
if len(queried_playlist) == 0:
|
||||
return jsonify({'error': "playlist doesn't exist"}), 400
|
||||
@ -89,7 +99,7 @@ def get_playlist():
|
||||
if len(queried_playlist) > 1:
|
||||
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
|
||||
|
||||
playlist_doc = playlists.document(queried_playlist[0].id)
|
||||
@ -102,10 +112,15 @@ def get_playlist():
|
||||
if playlist_id:
|
||||
dic['playlist_id'] = playlist_id
|
||||
|
||||
if playlist_shuffle is not None:
|
||||
dic['shuffle'] = playlist_shuffle
|
||||
|
||||
playlist_doc.update(dic)
|
||||
|
||||
return jsonify({"message": 'playlist updated', "status": "success"}), 200
|
||||
|
||||
|
||||
|
||||
else:
|
||||
return jsonify({'error': 'not logged in'}), 401
|
||||
|
||||
|
@ -78,7 +78,7 @@ def auth():
|
||||
|
||||
return redirect(urllib.parse.urlunparse(['https', 'accounts.spotify.com', 'authorize', '', params, '']))
|
||||
|
||||
return redirect('/')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@blueprint.route('/spotify/token')
|
||||
@ -116,7 +116,7 @@ def token():
|
||||
|
||||
return redirect('/app/settings/spotify')
|
||||
|
||||
return redirect('/')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@blueprint.route('/spotify/deauth')
|
||||
@ -134,4 +134,4 @@ def deauth():
|
||||
|
||||
return redirect('/app/settings/spotify')
|
||||
|
||||
return redirect('/')
|
||||
return redirect(url_for('index'))
|
||||
|
@ -23,9 +23,5 @@
|
||||
|
||||
<div id="react"></div>
|
||||
<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>
|
||||
</html>
|
@ -30,7 +30,7 @@
|
||||
<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>
|
||||
|
||||
<button class="full-width" onclick="handleLogin()" type="submit">go</button>
|
||||
<button class="button full-width" onclick="handleLogin()" type="submit">go</button>
|
||||
</form>
|
||||
<script src="{{ url_for('static', filename='js/login.bundle.js') }}"></script>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@ class Index extends Component{
|
||||
}
|
||||
|
||||
render(){
|
||||
return <h1 className="center-text">welcome to playlist manager!</h1>;
|
||||
return <h1 className="center-text text-no-select">welcome to playlist manager!</h1>;
|
||||
}
|
||||
}
|
||||
|
||||
|
93
src/js/Playlist/NewPlaylist.js
Normal file
93
src/js/Playlist/NewPlaylist.js
Normal 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;
|
@ -5,16 +5,19 @@ class PlaylistView extends Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
console.log(this.props.match.params.name);
|
||||
this.state = {
|
||||
name: this.props.match.params.name,
|
||||
parts: [],
|
||||
error: false,
|
||||
error_text: null,
|
||||
add_part_value: ''
|
||||
newPlaylistName: '',
|
||||
shuffle: false
|
||||
}
|
||||
this.handleAddPart = this.handleAddPart.bind(this);
|
||||
this.handleAddPartChange = this.handleAddPartChange.bind(this);
|
||||
this.handleRemoveRow = this.handleRemoveRow.bind(this);
|
||||
|
||||
this.handleShuffleChange = this.handleShuffleChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
@ -35,21 +38,46 @@ class PlaylistView extends Component{
|
||||
|
||||
handleAddPartChange(event){
|
||||
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){
|
||||
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({
|
||||
parts: parts
|
||||
});
|
||||
axios.post('/api/playlist', {
|
||||
name: this.state.name,
|
||||
parts: parts
|
||||
}).then((response) => {
|
||||
console.log(reponse);
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
@ -61,19 +89,33 @@ class PlaylistView extends Component{
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1>{ this.state.name }</h1></th>
|
||||
<th colSpan="2"><h1 className="text-no-select">{ this.state.name }</h1></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<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>
|
||||
<button className="button full-width" onClick={this.handleAddPart}>add</button>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
);
|
||||
@ -88,8 +130,8 @@ class PlaylistView extends Component{
|
||||
function Row (props) {
|
||||
return (
|
||||
<tr>
|
||||
<td className="ui-text center-text">{ props.part }</td>
|
||||
<td className="ui-text center-text">remove</td>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -1,19 +1,22 @@
|
||||
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');
|
||||
|
||||
import PlaylistsView from "./PlaylistsView.js"
|
||||
import NewPlaylist from "./NewPlaylist.js";
|
||||
|
||||
class Playlists extends Component {
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ class PlaylistsView extends Component {
|
||||
isLoading: true
|
||||
}
|
||||
this.getPlaylists();
|
||||
this.handleRunPlaylist = this.handleRunPlaylist.bind(this);
|
||||
this.handleDeletePlaylist = this.handleDeletePlaylist.bind(this);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
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>;
|
||||
|
||||
return this.state.isLoading ? loadingMessage : table;
|
||||
@ -36,7 +56,10 @@ function Table(props){
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<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>
|
||||
</table>
|
||||
);
|
||||
@ -46,6 +69,8 @@ function Row(props){
|
||||
return (
|
||||
<tr>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -62,15 +62,17 @@ class PlaylistManager extends Component {
|
||||
<div className="pad-9">
|
||||
<Switch>
|
||||
<Route path="/app" exact component={Index} />
|
||||
<Route path="/app/playlists" exact component={Playlists} />
|
||||
<Route path="/app/settings" render={(props) => <Settings {...props} spotify_linked={this.state.spotify_linked}/>} />
|
||||
<Route path="/app/playlists" component={Playlists} />
|
||||
<Route path="/app/settings" component={Settings} />
|
||||
{ this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> }
|
||||
<Route path='/app/playlist/:name' component={PlaylistView} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://github.com/Sarsoo/spotify-web">view source code</a>
|
||||
</footer>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
@ -84,12 +84,12 @@ class ChangePassword extends Component {
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1>change password</h1></th>
|
||||
<th colSpan="2"><h1 className="text-no-select">change password</h1></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text">current:</td>
|
||||
<td className="ui-text center-text text-no-select">current:</td>
|
||||
<td><input
|
||||
type="password"
|
||||
name="current"
|
||||
@ -98,7 +98,7 @@ class ChangePassword extends Component {
|
||||
className="full-width" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text">new:</td>
|
||||
<td className="ui-text center-text text-no-select">new:</td>
|
||||
<td><input
|
||||
type="password"
|
||||
name="new1"
|
||||
@ -107,7 +107,7 @@ class ChangePassword extends Component {
|
||||
className="full-width" /></td>
|
||||
</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
|
||||
type="password"
|
||||
name="new2"
|
||||
|
@ -15,7 +15,7 @@ class Settings extends Component {
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
);
|
||||
|
@ -6,25 +6,33 @@ class SpotifyLink extends Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
spotify_linked: props.spotify_linked
|
||||
spotify_linked: null,
|
||||
isLoading: true
|
||||
}
|
||||
this.getUserInfo();
|
||||
}
|
||||
|
||||
getUserInfo(){
|
||||
|
||||
axios.get('/api/user')
|
||||
.then((response) => {
|
||||
this.setState({
|
||||
spotify_linked: response.data.spotify_linked,
|
||||
isLoading: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
const table =
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text">
|
||||
<td className="ui-text center-text text-no-select">
|
||||
status: { this.state.spotify_linked ? "linked" : "unlinked" }
|
||||
</td>
|
||||
</tr>
|
||||
@ -34,8 +42,11 @@ class SpotifyLink extends Component {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
</table>;
|
||||
|
||||
const loadingMessage = <p className="center-text text-no-select">loading...</p>;
|
||||
|
||||
return this.state.isLoading ? loadingMessage : table;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
margin: 30px;
|
||||
}
|
||||
@ -273,7 +281,7 @@ ul.navbar {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
td {
|
||||
padding: $pad-px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,6 +289,14 @@ ul.navbar {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.text-no-select {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
p {
|
||||
text-align: right;
|
||||
|
Loading…
Reference in New Issue
Block a user