diff --git a/README.md b/README.md index 8b2ada38..d47249fc 100755 --- a/README.md +++ b/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. ![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¤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 ![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 ------- diff --git a/src/js/components/ArtistList.js b/src/js/components/ArtistList.js index fcd80dee..d9168f55 100755 --- a/src/js/components/ArtistList.js +++ b/src/js/components/ArtistList.js @@ -16,13 +16,21 @@ export default class ArtistList extends React.Component{
{ this.props.artists.map( (artist, index) => { - var link = '/artist/' + artist.uri; - return ( - - - { artist.name } - - ); + if( artist.uri ){ + return ( + + + { artist.name } + + ) + }else{ + return ( + + + { artist.name } + + ) + } }) }
diff --git a/src/js/components/Track.js b/src/js/components/Track.js index d722a49a..52348f2c 100755 --- a/src/js/components/Track.js +++ b/src/js/components/Track.js @@ -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 = {track.album.name} + } else { + album = {track.album.name} + } + } + return (
: '-' } - { track.album ? {track.album.name} : '-' } + {album} { track.duration_ms ? : null } diff --git a/src/js/index.js b/src/js/index.js index a3785265..5e6c64e6 100755 --- a/src/js/index.js +++ b/src/js/index.js @@ -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( + diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index fcd10b73..242d943d 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -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 diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 5b135963..922591f1 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -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 diff --git a/src/js/services/pusher/actions.js b/src/js/services/pusher/actions.js index 8d14de54..7e5f00a7 100755 --- a/src/js/services/pusher/actions.js +++ b/src/js/services/pusher/actions.js @@ -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 + } } \ No newline at end of file diff --git a/src/js/services/pusher/middleware.js b/src/js/services/pusher/middleware.js index d3015351..525aa47c 100755 --- a/src/js/services/pusher/middleware.js +++ b/src/js/services/pusher/middleware.js @@ -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': diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index b3b60c83..40ea7874 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -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 diff --git a/src/js/views/Debug.js b/src/js/views/Debug.js new file mode 100755 index 00000000..9c19ff67 --- /dev/null +++ b/src/js/views/Debug.js @@ -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 ( +
+
+ +
+ +

Mopidy

+
this.callMopidy(e)}> +
+
Call
+
+ this.setState({ mopidy_call: e.target.value })} + value={ this.state.mopidy_call } /> +
+
+
+
Data
+
+ +
+
+
+
+
+ +
+
+
+ +
+ +
+ +

Pusher

+
this.callPusher(e)}> +
+
Call
+
+ this.setState({ pusher_call: e.target.value })} + value={ this.state.pusher_call } /> +
+
+
+
Data
+
+ +
+
+
+
+
+ +
+
+
+ +
+ +
+

Response

+
+						{ this.props.ui.debug_response ? JSON.stringify(this.props.ui.debug_response, null, 2) : null }
+					
+
+
+ ); + } +} + + +/** + * 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) \ No newline at end of file diff --git a/src/js/views/Settings.js b/src/js/views/Settings.js index c7d549fd..941fd972 100755 --- a/src/js/views/Settings.js +++ b/src/js/views/Settings.js @@ -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{
- this.resetAllSettings()} /> + +
+
+
+ this.resetAllSettings()} /> +    + +
+
diff --git a/src/scss/global/_core.scss b/src/scss/global/_core.scss index 793c0ddf..476c4e2e 100755 --- a/src/scss/global/_core.scss +++ b/src/scss/global/_core.scss @@ -71,6 +71,11 @@ main { padding: 10px 0; } } + + pre { + padding: 20px; + background: $faint_grey; + } } h1 { diff --git a/src/scss/global/_forms.scss b/src/scss/global/_forms.scss index 231640d9..e13b8854 100755 --- a/src/scss/global/_forms.scss +++ b/src/scss/global/_forms.scss @@ -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; } } diff --git a/src/scss/views/_settings.scss b/src/scss/views/_settings.scss index 5406955d..1b947a82 100755 --- a/src/scss/views/_settings.scss +++ b/src/scss/views/_settings.scss @@ -47,4 +47,11 @@ padding: 20px; } } +} + + +.debugger-view { + section { + padding: 40px; + } } \ No newline at end of file