Mopidy service as middleware, prototype operational

This commit is contained in:
James Barnsley
2016-10-14 22:42:45 +13:00
parent 76df2ec503
commit ec7d6ae895
14 changed files with 347 additions and 143 deletions

View File

@ -1,5 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_URI} !/production/ [NC]
RewriteRule (.*) /production/$1 [NC,L]
RewriteRule (.*) /production/$1 [NC,L]

View File

@ -1,4 +1,3 @@
import albumActions from './albumActions'
import mopidy from './mopidy'

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

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