Debugger; Settings polish up; Album link on track when invalid uri
This commit is contained in:
44
README.md
44
README.md
@ -1,33 +1,17 @@
|
||||
Spotmop
|
||||
Iris
|
||||
=======
|
||||
|
||||
Mopidy web-based frontend that utilizes Spotify to create an interactive, user-friendly and collaborative music interface. Built and maintained by James Barnsley.
|
||||
Iris (previously known as Spotmop) is a Mopidy HTTP client that utilizes Spotify to create an interactive, user-friendly and collaborative music interface. Built and maintained by James Barnsley.
|
||||
|
||||

|
||||

|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=james%40barnsley%2enz&lc=NZ&item_name=James%20Barnsley¤cy_code=NZD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted)
|
||||
|
||||
|
||||
Requirements
|
||||
--------
|
||||
|
||||
* Mopidy
|
||||
* Mopidy-Spotify
|
||||
* Spotify Premium account
|
||||
* Mopidy-Local-Sqlite (recommended, not required)
|
||||
|
||||
Installation
|
||||
--------
|
||||
|
||||
1. Install using pip: `sudo pip install Mopidy-Spotmop`
|
||||
2. Restart Mopidy server
|
||||
3. Navigate to Mopidy interface (ie http://localhost:6680/spotmop)
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Full web-based interface controls for Mopidy
|
||||
* Uses Spotify API to deliver high-quality audio and music information
|
||||
* Integrated with Spotify and LastFM APIs for high-quality artwork and extra info
|
||||
* Improved support for local libraries using SQLite extension
|
||||
* Browse and manage your playlists, along with top tracks, new releases and genre browser
|
||||
* Spotmop can be run completely independently of your Mopidy machine (ie on a remote server), just set your URL in the settings tab
|
||||
@ -49,11 +33,27 @@ Screenshots
|
||||

|
||||
|
||||
|
||||
To-do
|
||||
Requirements
|
||||
--------
|
||||
|
||||
* Mopidy
|
||||
* Mopidy-Spotify
|
||||
* Spotify Premium account
|
||||
* Mopidy-Local-Sqlite (recommended, not required)
|
||||
|
||||
Installation
|
||||
--------
|
||||
|
||||
1. Install using pip: `sudo pip install Mopidy-Spotmop`
|
||||
2. Restart Mopidy server
|
||||
3. Navigate to Mopidy interface (ie http://localhost:6680/spotmop)
|
||||
|
||||
Development
|
||||
-----
|
||||
|
||||
* Increase stability of Mopidy server (perhaps limitation of Rpi?)
|
||||
* Improve websockets integration to attach users to tracks and changes within tracklist
|
||||
1. Clone this repository into a your Apache2 web directory (ie `/var/www/html/`)
|
||||
2. Install in develop mode `python setup.py install develop`
|
||||
3. Restart mopidy
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
@ -16,13 +16,21 @@ export default class ArtistList extends React.Component{
|
||||
<div className="artist-list">
|
||||
{
|
||||
this.props.artists.map( (artist, index) => {
|
||||
var link = '/artist/' + artist.uri;
|
||||
return (
|
||||
<Link to={ link } key={artist.uri} className="artist">
|
||||
<Thumbnail circle={true} size="small" images={artist.images} />
|
||||
<span className="name">{ artist.name }</span>
|
||||
</Link>
|
||||
);
|
||||
if( artist.uri ){
|
||||
return (
|
||||
<Link to={ '/artist/' + artist.uri } key={artist.uri} className="artist">
|
||||
<Thumbnail circle={true} size="small" images={artist.images} />
|
||||
<span className="name">{ artist.name }</span>
|
||||
</Link>
|
||||
)
|
||||
}else{
|
||||
return (
|
||||
<span key={artist.uri} className="artist">
|
||||
<Thumbnail circle={true} size="small" images={artist.images} />
|
||||
<span className="name">{ artist.name }</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -41,6 +41,15 @@ export default class Track extends React.Component{
|
||||
if( typeof(track.selected) !== 'undefined' && track.selected ) className += ' selected';
|
||||
if( track.playing ) className += ' playing';
|
||||
|
||||
var album = '-'
|
||||
if( track.album ){
|
||||
if( track.album.uri ){
|
||||
album = <Link to={'/album/'+track.album.uri}>{track.album.name}</Link>
|
||||
} else {
|
||||
album = <span>{track.album.name}</span>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
@ -56,7 +65,7 @@ export default class Track extends React.Component{
|
||||
{ track.artists ? <ArtistSentence artists={track.artists} /> : '-' }
|
||||
</span>
|
||||
<span className="col album">
|
||||
{ track.album ? <Link to={'/album/'+track.album.uri}>{track.album.name}</Link> : '-' }
|
||||
{album}
|
||||
</span>
|
||||
<span className="col duration">
|
||||
{ track.duration_ms ? <Dater type="length" data={track.duration_ms} /> : null }
|
||||
|
||||
@ -19,6 +19,7 @@ import Playlist from './views/Playlist'
|
||||
import User from './views/User'
|
||||
import Queue from './views/Queue'
|
||||
import Settings from './views/Settings'
|
||||
import Debug from './views/Debug'
|
||||
import Search from './views/Search'
|
||||
|
||||
import Discover from './views/discover/Discover'
|
||||
@ -44,6 +45,7 @@ ReactDOM.render(
|
||||
<IndexRoute component={Queue} />
|
||||
<Route path="queue" component={Queue} />
|
||||
<Route path="settings" component={Settings} />
|
||||
<Route path="debug" component={Debug} />
|
||||
|
||||
<Route path="search/:query" component={Search} />
|
||||
<Route path="search/:query/:type" component={Search} />
|
||||
|
||||
@ -28,6 +28,14 @@ export function instruct( call, value ){
|
||||
}
|
||||
}
|
||||
|
||||
export function debug( call, value ){
|
||||
return {
|
||||
type: 'MOPIDY_DEBUG',
|
||||
call: call,
|
||||
value: value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Playback-oriented actions
|
||||
|
||||
@ -161,6 +161,14 @@ const MopidyMiddleware = (function(){
|
||||
instruct( socket, store, action.call, action.value )
|
||||
break;
|
||||
|
||||
// send an instruction to the websocket
|
||||
case 'MOPIDY_DEBUG':
|
||||
instruct( socket, store, action.call, action.value )
|
||||
.then( response => {
|
||||
store.dispatch({ type: 'DEBUG', response: response })
|
||||
})
|
||||
break;
|
||||
|
||||
// send an instruction to the websocket
|
||||
case 'MOPIDY_NEXT':
|
||||
var icon = false
|
||||
|
||||
@ -49,4 +49,12 @@ export function instruct( action, data = null ){
|
||||
action: action,
|
||||
data: data
|
||||
}
|
||||
}
|
||||
|
||||
export function debug( call, data = null ){
|
||||
return {
|
||||
type: 'PUSHER_DEBUG',
|
||||
call: call,
|
||||
data: data
|
||||
}
|
||||
}
|
||||
@ -87,7 +87,35 @@ const PusherMiddleware = (function(){
|
||||
break;
|
||||
|
||||
case 'PUSHER_INSTRUCT':
|
||||
makeRequest({ action: action.action, data: action.data });
|
||||
switch( action.action ){
|
||||
case 'query':
|
||||
makeRequest( action.data )
|
||||
break
|
||||
case 'broadcast':
|
||||
broadcast( action.data )
|
||||
break
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUSHER_DEBUG':
|
||||
switch( action.call ){
|
||||
case 'query':
|
||||
makeRequest( action.data )
|
||||
// THIS DOES NOT RETURN A PROMISE SO CAN'T DETECT RELATED RESPONSES
|
||||
/*.then( response => {
|
||||
store.dispatch({ type: 'DEBUG', response: response })
|
||||
})*/
|
||||
break
|
||||
case 'broadcast':
|
||||
broadcast( action.data )
|
||||
/*.then( response => {
|
||||
store.dispatch({ type: 'DEBUG', response: response })
|
||||
})*/
|
||||
break
|
||||
default:
|
||||
store.dispatch({ type: 'DEBUG', response: '{ "error": "Invalid call" }' })
|
||||
break
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUSHER_SEND_BROADCAST':
|
||||
|
||||
@ -7,6 +7,9 @@ export default function reducer(ui = {}, action){
|
||||
case 'LAZY_LOADING':
|
||||
return Object.assign({}, ui, { lazy_loading: action.start });
|
||||
|
||||
case 'DEBUG':
|
||||
return Object.assign({}, ui, { debug_response: action.response })
|
||||
|
||||
|
||||
/**
|
||||
* UI views and view modes
|
||||
|
||||
160
src/js/views/Debug.js
Executable file
160
src/js/views/Debug.js
Executable file
@ -0,0 +1,160 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import SpotifyAuthenticationFrame from '../components/SpotifyAuthenticationFrame'
|
||||
import ConfirmationButton from '../components/ConfirmationButton'
|
||||
import PusherConnectionList from '../components/PusherConnectionList'
|
||||
import URISchemesList from '../components/URISchemesList'
|
||||
import VersionManager from '../components/VersionManager'
|
||||
import Header from '../components/Header'
|
||||
import Thumbnail from '../components/Thumbnail'
|
||||
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as pusherActions from '../services/pusher/actions'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
|
||||
class Debug extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mopidy_call: 'playlists.asList',
|
||||
mopidy_data: '{}',
|
||||
pusher_call: 'broadcast',
|
||||
pusher_data: '{}'
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if( this.props.pusher.connectionid ){
|
||||
var data = {
|
||||
action: "notification",
|
||||
recipients: [this.props.pusher.connectionid],
|
||||
data: {
|
||||
title: "Title",
|
||||
body: "Test notification",
|
||||
icon: "http://lorempixel.com/100/100/nature/"
|
||||
}
|
||||
}
|
||||
this.setState({ pusher_data: JSON.stringify(data) })
|
||||
}
|
||||
}
|
||||
|
||||
callMopidy(e){
|
||||
e.preventDefault()
|
||||
console.info('Mopidy Debugger', this.state.mopidy_call, JSON.parse(this.state.mopidy_data) )
|
||||
this.props.mopidyActions.debug( this.state.mopidy_call, JSON.parse(this.state.mopidy_data) )
|
||||
}
|
||||
|
||||
callPusher(e){
|
||||
e.preventDefault()
|
||||
console.info('Pusher Debugger', this.state.pusher_call, JSON.parse(this.state.pusher_data) )
|
||||
this.props.pusherActions.debug( this.state.pusher_call, JSON.parse(this.state.pusher_data) )
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="view debugger-view">
|
||||
<Header icon="cog" title="Debugger" />
|
||||
|
||||
<section>
|
||||
|
||||
<h4 className="underline">Mopidy</h4>
|
||||
<form onSubmit={(e) => this.callMopidy(e)}>
|
||||
<div className="field">
|
||||
<div className="name">Call</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={ e => this.setState({ mopidy_call: e.target.value })}
|
||||
value={ this.state.mopidy_call } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="name">Data</div>
|
||||
<div className="input">
|
||||
<textarea
|
||||
onChange={ e => this.setState({ mopidy_data: e.target.value })}
|
||||
value={ this.state.mopidy_data }>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="name"></div>
|
||||
<div className="input">
|
||||
<button type="submit" className="secondary">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
<h4 className="underline">Pusher</h4>
|
||||
<form onSubmit={(e) => this.callPusher(e)}>
|
||||
<div className="field">
|
||||
<div className="name">Call</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={ e => this.setState({ pusher_call: e.target.value })}
|
||||
value={ this.state.pusher_call } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="name">Data</div>
|
||||
<div className="input">
|
||||
<textarea
|
||||
onChange={ e => this.setState({ pusher_data: e.target.value })}
|
||||
value={ this.state.pusher_data }>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="name"></div>
|
||||
<div className="input">
|
||||
<button type="submit" className="secondary">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="underline">Response</h4>
|
||||
<pre>
|
||||
{ this.props.ui.debug_response ? JSON.stringify(this.props.ui.debug_response, null, 2) : null }
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
pusherActions: bindActionCreators(pusherActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Debug)
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router'
|
||||
import { browserHistory, Link } from 'react-router'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
@ -247,7 +247,17 @@ class Settings extends React.Component{
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmationButton content="Reset all settings" confirmingContent="Are you sure?" onConfirm={() => this.resetAllSettings()} />
|
||||
|
||||
<div className="field">
|
||||
<div className="name"></div>
|
||||
<div className="input">
|
||||
<ConfirmationButton className="destructive" content="Reset all settings" confirmingContent="Are you sure?" onConfirm={() => this.resetAllSettings()} />
|
||||
|
||||
<button className="primary" onClick={ () => browserHistory.push('debug') }>
|
||||
<FontAwesome name="flask" /> Debugger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -71,6 +71,11 @@ main {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 20px;
|
||||
background: $faint_grey;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@ -125,13 +125,15 @@ input[type="submit"] {
|
||||
width: 85%;
|
||||
float: left;
|
||||
|
||||
input {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
padding: 10px 14px;
|
||||
padding: 10px 14px 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,4 +47,11 @@
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.debugger-view {
|
||||
section {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user