Debugger; Settings polish up; Album link on track when invalid uri

This commit is contained in:
James Barnsley
2016-12-05 10:20:01 +13:00
parent 5f07e855bd
commit 33619c6b7e
14 changed files with 293 additions and 35 deletions

View File

@ -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.
![badge](https://img.shields.io/pypi/v/mopidy-spotmop.svg?style=flat)
![badge](https://img.shields.io/pypi/dm/mopidy-spotmop.svg)
[![badge](https://img.shields.io/badge/donate-paypal-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=james%40barnsley%2enz&lc=NZ&item_name=James%20Barnsley&currency_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
![Dragging tracks](https://raw.githubusercontent.com/jaedb/spotmop/master/Screenshots/desktop-dragging.jpg)
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
-------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()} />
&nbsp;&nbsp;
<button className="primary" onClick={ () => browserHistory.push('debug') }>
<FontAwesome name="flask" />&nbsp;Debugger
</button>
</div>
</div>
</section>
</div>

View File

@ -71,6 +71,11 @@ main {
padding: 10px 0;
}
}
pre {
padding: 20px;
background: $faint_grey;
}
}
h1 {

View File

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

View File

@ -47,4 +47,11 @@
padding: 20px;
}
}
}
.debugger-view {
section {
padding: 40px;
}
}