Mopidy service as middleware, prototype operational
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
RewriteCond %{REQUEST_URI} !/production/ [NC]
|
||||
RewriteRule (.*) /production/$1 [NC,L]
|
||||
RewriteRule (.*) /production/$1 [NC,L]
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import albumActions from './albumActions'
|
||||
import mopidy from './mopidy'
|
||||
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export function updateStatus( online ){
|
||||
return {
|
||||
type: 'STATUS',
|
||||
online: online
|
||||
}
|
||||
}
|
||||
|
||||
export function updateState( state ){
|
||||
return {
|
||||
type: 'STATE',
|
||||
state: state
|
||||
}
|
||||
}
|
||||
|
||||
export function updateConsume( consume ){
|
||||
return {
|
||||
type: 'CONSUME',
|
||||
consume: consume
|
||||
}
|
||||
}
|
||||
|
||||
export function updateRandom( random ){
|
||||
return {
|
||||
type: 'RANDOM',
|
||||
random: random
|
||||
}
|
||||
}
|
||||
|
||||
export function updateRepeat( repeat ){
|
||||
return {
|
||||
type: 'REPEAT',
|
||||
repeat: repeat
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTlTracks( tracks ){
|
||||
return {
|
||||
type: 'TRACKLIST',
|
||||
tracks: tracks
|
||||
}
|
||||
}
|
||||
|
||||
export function updateCurrentTlTrack( tltrack ){
|
||||
return {
|
||||
type: 'TRACKINFOCUS',
|
||||
trackInFocus: tltrack
|
||||
}
|
||||
}
|
||||
|
||||
export function updateVolume( volume ){
|
||||
return {
|
||||
type: 'VOLUME',
|
||||
volume: volume
|
||||
}
|
||||
}
|
||||
|
||||
export function changeTrack( tlid ){
|
||||
return {
|
||||
type: 'CHANGE_TRACK',
|
||||
tlid: tlid
|
||||
}
|
||||
}
|
||||
|
||||
10
src/js/bootstrap.js
vendored
10
src/js/bootstrap.js
vendored
@ -1,10 +1,16 @@
|
||||
|
||||
console.info('Bootstrapping...');
|
||||
|
||||
import { createStore } from 'redux'
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import reducer from './reducers/index'
|
||||
import mopidyMiddleware from './services/mopidy/middleware'
|
||||
|
||||
// create our global store
|
||||
let store = createStore( reducer, {} );
|
||||
let store = createStore(
|
||||
reducer,
|
||||
{},
|
||||
applyMiddleware( thunk, mopidyMiddleware )
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
||||
@ -6,8 +6,7 @@ import { Link } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import * as actions from '../actions/index'
|
||||
import Services from '../services/Services'
|
||||
import MopidyService from '../services/MopidyService'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
|
||||
|
||||
/**
|
||||
@ -19,6 +18,10 @@ class App extends React.Component{
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.props.mopidyActions.connect();
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
return (
|
||||
@ -32,7 +35,6 @@ class App extends React.Component{
|
||||
<li><a onClick={this.handleClick}>Authorize</a></li>
|
||||
</ul>
|
||||
{this.props.children}
|
||||
<MopidyService />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -50,7 +52,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
76
src/js/components/Player.js
Executable file
76
src/js/components/Player.js
Executable file
@ -0,0 +1,76 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import * as actions from '../services/mopidy/actions'
|
||||
|
||||
class Player extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderControls(){
|
||||
|
||||
var playButton = <a onClick={() => this.props.actions.instruct('playback.play')}><FontAwesome name="play" /> </a>
|
||||
if( this.props.mopidy.state == 'playing' ){
|
||||
playButton = <a onClick={() => this.props.actions.instruct('playback.pause')}><FontAwesome name="pause" /> </a>
|
||||
}
|
||||
|
||||
var consumeButton = <a onClick={() => this.props.actions.instruct('tracklist.setConsume', [true])}>Consume </a>
|
||||
if( this.props.mopidy.consume ){
|
||||
consumeButton = <a onClick={() => this.props.actions.instruct('tracklist.setConsume', [false])}>Un-Consume </a>
|
||||
}
|
||||
|
||||
var randomButton = <a onClick={() => this.props.actions.instruct('tracklist.setRandom', [true])}>Random </a>
|
||||
if( this.props.mopidy.random ){
|
||||
randomButton = <a onClick={() => this.props.actions.instruct('tracklist.setRandom', [false])}>Un-Random </a>
|
||||
}
|
||||
|
||||
var repeatButton = <a onClick={() => this.props.actions.instruct('tracklist.setRepeat', [true])}>Repeat </a>
|
||||
if( this.props.mopidy.repeat ){
|
||||
repeatButton = <a onClick={() => this.props.actions.instruct('tracklist.setRepeat', [false])}>Un-Repeat </a>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ playButton }
|
||||
<a onClick={() => this.props.actions.instruct('playback.previous')}><FontAwesome name="step-backward" /> </a>
|
||||
<a onClick={() => this.props.actions.instruct('playback.next')}><FontAwesome name="step-forward" /> </a>
|
||||
{ consumeButton }
|
||||
{ randomButton }
|
||||
{ repeatButton }
|
||||
{ this.props.mopidy.volume }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
{ this.renderControls() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Player)
|
||||
@ -3,10 +3,11 @@ import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Services from '../services/Services'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import TrackList from '../components/TrackList'
|
||||
import Track from '../components/Track'
|
||||
import * as actions from '../actions/mopidy'
|
||||
import Player from '../components/Player'
|
||||
import * as actions from '../services/mopidy/actions'
|
||||
|
||||
class Queue extends React.Component{
|
||||
|
||||
@ -39,6 +40,7 @@ class Queue extends React.Component{
|
||||
{ this.renderTrackInFocus() }
|
||||
<h4>Other tracks</h4>
|
||||
{ this.renderTrackList() }
|
||||
<Player />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as actions from '../actions/mopidy'
|
||||
import * as actions from '../services/mopidy/actions'
|
||||
|
||||
import Track from './Track'
|
||||
import MopidyService from '../services/MopidyService'
|
||||
|
||||
class TrackList extends React.Component{
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
// import all of our application reducers
|
||||
import album from './album'
|
||||
import mopidy from './mopidy'
|
||||
import mopidy from '../services/mopidy/reducer'
|
||||
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
|
||||
export default function reducer(mopidy = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case 'STATUS':
|
||||
return Object.assign({}, mopidy, {
|
||||
online: action.online
|
||||
});
|
||||
|
||||
case 'STATE':
|
||||
return Object.assign({}, mopidy, {
|
||||
state: action.state
|
||||
});
|
||||
|
||||
case 'CONSUME':
|
||||
return Object.assign({}, mopidy, {
|
||||
consume: action.consume
|
||||
});
|
||||
|
||||
case 'RANDOM':
|
||||
return Object.assign({}, mopidy, {
|
||||
random: action.random
|
||||
});
|
||||
|
||||
case 'REPEAT':
|
||||
return Object.assign({}, mopidy, {
|
||||
repeat: action.repeat
|
||||
});
|
||||
|
||||
case 'TRACKLIST':
|
||||
return Object.assign({}, mopidy, {
|
||||
tracks: action.tracks
|
||||
});
|
||||
|
||||
case 'TRACKINFOCUS':
|
||||
console.log( action );
|
||||
return Object.assign({}, mopidy, {
|
||||
trackInFocus: action.trackInFocus
|
||||
});
|
||||
|
||||
case 'VOLUME':
|
||||
return Object.assign({}, mopidy, {
|
||||
volume: action.volume
|
||||
});
|
||||
|
||||
case 'CHANGE_TRACK':
|
||||
return Object.assign({}, mopidy, {
|
||||
tlid: action.tlid
|
||||
});
|
||||
|
||||
default:
|
||||
return mopidy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import * as actions from '../actions/mopidy'
|
||||
import * as actions from '../services/mopidy/actions'
|
||||
|
||||
|
||||
/**
|
||||
@ -16,14 +16,21 @@ import * as actions from '../actions/mopidy'
|
||||
class MopidyService extends React.Component{
|
||||
|
||||
constructor( props ){
|
||||
super(props)
|
||||
|
||||
super(props);
|
||||
/*
|
||||
this.connection = new Mopidy({
|
||||
webSocketUrl: "ws://music.james:6680/mopidy/ws",
|
||||
webSocketUrl: "ws://tv.barnsley.nz:6680/mopidy/ws",
|
||||
//webSocketUrl: "ws://music.james:6680/mopidy/ws",
|
||||
callingConvention: 'by-position-or-by-name'
|
||||
});
|
||||
|
||||
this.connection.on( (type, data) => this.handleMessage( type, data ) );
|
||||
*/
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
|
||||
this.props.actions.connect();
|
||||
}
|
||||
|
||||
handleMessage( type, data ){
|
||||
|
||||
25
src/js/services/mopidy/actions.js
Executable file
25
src/js/services/mopidy/actions.js
Executable file
@ -0,0 +1,25 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export function connect(){
|
||||
return {
|
||||
type: 'CONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnect(){
|
||||
return {
|
||||
type: 'DISCONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function instruct( call, value ){
|
||||
return {
|
||||
type: 'INSTRUCT',
|
||||
call: call,
|
||||
value: value
|
||||
}
|
||||
}
|
||||
|
||||
153
src/js/services/mopidy/middleware.js
Executable file
153
src/js/services/mopidy/middleware.js
Executable file
@ -0,0 +1,153 @@
|
||||
|
||||
import Mopidy from 'mopidy'
|
||||
import actions from './actions'
|
||||
|
||||
const MopidyMiddleware = (function(){
|
||||
|
||||
var socket = null;
|
||||
|
||||
const onOpen = (ws, store, token) => evt => {
|
||||
console.log('opened')
|
||||
//Send a handshake, or authenticate with remote end
|
||||
|
||||
//Tell the store we're connected
|
||||
//store.dispatch(actions.connected());
|
||||
}
|
||||
|
||||
const onClose = (ws, store) => evt => {
|
||||
//Tell the store we've disconnected
|
||||
//store.dispatch(actions.disconnected());
|
||||
}
|
||||
|
||||
const handleMessage = (ws, store, type, data) => {
|
||||
|
||||
switch( type ){
|
||||
|
||||
case 'state:online':
|
||||
//store.dispatch( actions.updateStatus( true ) );
|
||||
instruct( ws, store, 'playback.getState' );
|
||||
instruct( ws, store, 'playback.getVolume' );
|
||||
instruct( ws, store, 'tracklist.getConsume' );
|
||||
instruct( ws, store, 'tracklist.getRandom' );
|
||||
instruct( ws, store, 'tracklist.getRepeat' );
|
||||
instruct( ws, store, 'tracklist.getTlTracks' );
|
||||
instruct( ws, store, 'playback.getCurrentTlTrack' );
|
||||
break;
|
||||
|
||||
case 'state:offline':
|
||||
store.dispatch( actions.updateStatus( false ) );
|
||||
break;
|
||||
|
||||
case 'event:tracklistChanged':
|
||||
instruct( ws, store, 'tracklist.getTlTracks' );
|
||||
break;
|
||||
|
||||
//case 'event:trackPlaybackEnded':
|
||||
case 'event:playbackStateChanged':
|
||||
case 'event:trackPlaybackStarted':
|
||||
instruct( ws, store, 'playback.getState' );
|
||||
instruct( ws, store, 'playback.getCurrentTlTrack' );
|
||||
break;
|
||||
|
||||
case 'event:volumeChanged':
|
||||
store.dispatch( actions.updateVolume( data.volume ) );
|
||||
break;
|
||||
|
||||
case 'event:optionsChanged':
|
||||
instruct( ws, store, 'tracklist.getConsume' );
|
||||
instruct( ws, store, 'tracklist.getRandom' );
|
||||
instruct( ws, store, 'tracklist.getRepeat' );
|
||||
break;
|
||||
|
||||
default:
|
||||
//console.log( 'MopidyService: Unhandled event', type, message );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function doStuff( ws, store ){
|
||||
console.log('doing stuff', store);
|
||||
ws.playback.getState()
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({ type: 'STATE', state: response })
|
||||
},
|
||||
error => {
|
||||
console.error( error );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get something from Mopidy
|
||||
*
|
||||
* Sends request to Mopidy server, and updates our local storage on return
|
||||
* @param object ws Websocket
|
||||
* @param string model Mopidy model (playback, tracklist, etc)
|
||||
* @param string property the property to get (TlTracks, Consume, etc)
|
||||
**/
|
||||
const instruct = ( ws, store, call, value = false ) => {
|
||||
console.log('Mopidy: '+call, value);
|
||||
var callParts = call.split('.');
|
||||
var model = callParts[0];
|
||||
var method = callParts[1];
|
||||
var property = method;
|
||||
property = property.replace('get','');
|
||||
property = property.replace('set','');
|
||||
|
||||
if( value ){
|
||||
ws[model][method]( value )
|
||||
.then(
|
||||
response => store.dispatch({ type: property, model: model, data: response }),
|
||||
error => console.error( error )
|
||||
);
|
||||
}else{
|
||||
ws[model][method]()
|
||||
.then(
|
||||
response => store.dispatch({ type: property, model: model, data: response }),
|
||||
error => console.error( error )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return store => next => action => {
|
||||
|
||||
switch(action.type) {
|
||||
|
||||
//The user wants us to connect
|
||||
case 'CONNECT':
|
||||
|
||||
if(socket != null) socket.close();
|
||||
store.dispatch({ type: 'CONNECTING' });
|
||||
|
||||
socket = new Mopidy({
|
||||
webSocketUrl: "ws://tv.barnsley.nz:6680/mopidy/ws",
|
||||
callingConvention: 'by-position-or-by-name'
|
||||
});
|
||||
|
||||
socket.on( (type, data) => handleMessage( socket, store, type, data ) );
|
||||
|
||||
break;
|
||||
|
||||
//The user wants us to disconnect
|
||||
case 'DISCONNECT':
|
||||
if(socket != null) socket.close();
|
||||
socket = null;
|
||||
store.dispatch({ type: 'DISCONNECTED' });
|
||||
break;
|
||||
|
||||
//Send the 'SEND_MESSAGE' action down the websocket to the server
|
||||
case 'INSTRUCT':
|
||||
instruct( socket, store, action.call, action.value )
|
||||
break;
|
||||
|
||||
//This action is irrelevant to us, pass it on to the next middleware
|
||||
default:
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
export default MopidyMiddleware
|
||||
60
src/js/services/mopidy/reducer.js
Executable file
60
src/js/services/mopidy/reducer.js
Executable file
@ -0,0 +1,60 @@
|
||||
|
||||
export default function reducer(mopidy = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case 'CONNECTING':
|
||||
return Object.assign({}, mopidy, { connected: false, connecting: true });
|
||||
|
||||
case 'CONNECTED':
|
||||
return Object.assign({}, mopidy, { connected: true, connecting: false });
|
||||
|
||||
case 'DISCONNECTED':
|
||||
return Object.assign({}, mopidy, { connected: false, connecting: false });
|
||||
|
||||
case 'State':
|
||||
return Object.assign({}, mopidy, {
|
||||
state: action.data
|
||||
});
|
||||
|
||||
case 'Consume':
|
||||
return Object.assign({}, mopidy, {
|
||||
consume: action.data
|
||||
});
|
||||
|
||||
case 'Random':
|
||||
return Object.assign({}, mopidy, {
|
||||
random: action.data
|
||||
});
|
||||
|
||||
case 'Repeat':
|
||||
return Object.assign({}, mopidy, {
|
||||
repeat: action.data
|
||||
});
|
||||
|
||||
case 'TlTracks':
|
||||
return Object.assign({}, mopidy, {
|
||||
tracks: action.data
|
||||
});
|
||||
|
||||
case 'CurrentTlTrack':
|
||||
return Object.assign({}, mopidy, {
|
||||
trackInFocus: action.data
|
||||
});
|
||||
|
||||
case 'Volume':
|
||||
return Object.assign({}, mopidy, {
|
||||
volume: action.volume
|
||||
});
|
||||
|
||||
case 'CHANGE_TRACK':
|
||||
return Object.assign({}, mopidy, {
|
||||
tlid: action.tlid
|
||||
});
|
||||
|
||||
default:
|
||||
return mopidy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user